{"record":{"id":"2c925b7ad1e02e55","repo":"emberjs/ember.js","slug":"an-unquoted-attribute-value-must-be-a-string-or-a","errorCode":null,"errorMessage":"An unquoted attribute value must be a string or a mustache, preceded by whitespace or a '=' character, and followed by whitespace, a '>' character, or '/>'","messagePattern":"An unquoted attribute value must be a string or a mustache, preceded by whitespace or a '=' character, and followed by whitespace, a '>' character, or '/>'","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts","lineNumber":617,"sourceCode":"  }\n\n  assembleAttributeValue(\n    parts: ASTv1.AttrPart[],\n    isQuoted: boolean,\n    isDynamic: boolean,\n    span: src.SourceSpan\n  ): ASTv1.AttrValue {\n    if (isDynamic) {\n      if (isQuoted) {\n        return this.assembleConcatenatedValue(parts);\n      } else {\n        assertPresentArray(parts);\n\n        const [head, a] = parts;\n        if (a === undefined || (a.type === 'TextNode' && a.chars === '/')) {\n          return head;\n        } else {\n          throw generateSyntaxError(\n            `An unquoted attribute value must be a string or a mustache, ` +\n              `preceded by whitespace or a '=' character, and ` +\n              `followed by whitespace, a '>' character, or '/>'`,\n            span\n          );\n        }\n      }\n    } else if (isPresentArray(parts)) {\n      return parts[0];\n    } else {\n      return b.text({ chars: '', loc: span });\n    }\n  }\n}\n\n/**\n  ASTPlugins can make changes to the Glimmer template AST before\n  compilation begins.","sourceCodeStart":599,"sourceCodeEnd":635,"githubUrl":"https://github.com/emberjs/ember.js/blob/26f97246a8bf2e28edf26ac3093da2e86c04ffc5/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts#L599-L635","documentation":"assembleAttributeValue, when assembling an unquoted attribute value from concatenated parts (text/mustache), only allows a single part (or a lone `/`). If more than one part remains after simplification — e.g. an unquoted value mixing text and mustaches — the value is invalid for unquoted form and this error is thrown.","triggerScenarios":"Unquoted attribute value containing a mustache plus extra text or a trailing slash, e.g. `<div class=\"a{{x}}\">` written as `<div class=a{{x}}b>` or `<div foo=bar/>`-style oddities.","commonSituations":"Writing dynamic attribute values without quotes; forgetting quotes after refactoring `<div class={{x}}>rest`; shell-style `/` at the end of an unquoted value.","solutions":["Quote the attribute value: `class=\"a{{x}}b\"`","Use a single mustache or single text node for unquoted values, e.g. `class={{x}}`","Build the combined string in JS and pass one value instead"],"exampleFix":"// before\n<div class=a{{x}}b>...</div>\n// after\n<div class=\"a{{x}}b\">...</div>","handlingStrategy":"validation","validationCode":"// unquoted attr values with interpolation are invalid\nif (/<\\w+\\s+[\\w-]+=[^\\s\"'>{]*\\{\\{/m.test(template)) {\n  throw new Error('Quote attribute values containing mustaches');\n}","typeGuard":null,"tryCatchPattern":"try { parse(template) } catch (e) { if (/unquoted attribute value/.test(e.message)) { /* auto-quote the attribute and reparse */ } else throw e; }","preventionTips":["Always quote dynamic attribute values","Use single-mustache form `attr={{value}}` when unquoted","Lint for unquoted attributes"],"tags":["glimmer","handlebars","template-syntax","attribute-value"],"backgroundTag":"invalid-unquoted-attribute","analyzedSha":"26f97246a8bf2e28edf26ac3093da2e86c04ffc5","analyzedAt":"2026-09-01T05:01:28.182Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}