Skip to main content
${ } normally comes out escaped for wherever it landed. Attributes and <script> bodies are where that stops being uniform: a browser reads those two places with different parsers, and no single escaper is correct in both. For a fix to error[UnsafeScriptInterpolation], skip to Choosing between | js and | json.

Ordinary attributes need nothing

Any quoted attribute interpolates, and the value is escaped for an attribute value on the way out.
A data- attribute is the simplest way to hand a value to client-side code: publish it in markup, read it back with dataset in your own script. Nothing on this page applies to it.
slurp build minifies its output, so the HTML on disk may drop quotes and decode entities that are unambiguous in position (data-product-id=42). The examples here show what the compiler emits, before that pass. Both forms parse identically.

Interpolation in a <script> body

Write an interpolation in a script body with no filter and the build fails:
<script> is a raw text element: the HTML parser hands its contents to the JavaScript engine without decoding character references. So the entity escaping that makes ${ } safe in ordinary text does nothing at all there, and every JavaScript metacharacter except < passes through untouched. There is no single correct escaper either, because a value can land inside a string literal or in code position, and those need different treatments. So the compiler refuses rather than picking one. Two filters satisfy the rule, | js and | json. | unsafe_js does not.

Choosing between | js and | json

One question decides it: Did you write quotes around the slot?

Yes: use | js

The value goes inside a string literal you wrote. | js backslash-escapes the JavaScript metacharacters and adds no quotes of its own.

No: use | json

The value stands alone in code position. | json emits a complete, self-delimiting JSON literal, so a string arrives with its own quotes.
Both are correct and both produce the same JavaScript string. | json also handles values that are not strings, which | js does not:
When in doubt, write | json with no quotes around it. It is self-delimiting whatever the value turns out to be, including an object, a number, or nothing at all.

Using | js in code position

| js in code position compiles, because the filter is present and that is all the script-body rule checks. What it emits is a bare, unquoted value:
That is a syntax error in the browser, and with a different value it would be worse than a syntax error. Numbers happen to survive this mistake, which is how it reaches production: var n = ${ product.id | js } renders var n = 42; and works fine right up until the field is a string.

A JavaScript template literal cannot be written in a script body

Slurp claims the ${ sequence itself, so there is no way to write a backtick template in a script body. The compiler says so as part of the same error:
Concatenate instead: let t = hours + "h";.

Passing a whole object to the page

For anything larger than a value or two, do not build JavaScript at all. Emit a JSON payload and parse it:
Then in cart.js:
This keeps the JavaScript in a .js file where an editor and a linter can see it, and the template generates only data. {$let name = expr} does the same thing with slurp’s own spelling, emitting a <script type="application/json" data-slurp-let="name"> payload for the browser runtime to pick up.

JavaScript-evaluated attributes

An attribute counts as JavaScript-evaluated if it is a native on* handler or one of the Alpine forms. The full set:
  • anything starting with @ or x-on:
  • anything starting with : or x-bind:
  • any name longer than two characters starting with on
  • x-data x-init x-effect x-show x-text x-html x-if x-for x-model
Modifier suffixes are stripped, so x-model.lazy is recognised. Here the compiler does not refuse, because it can read the author’s own text around the slot and work out where the value lands. Position decides the filter, and the same question from the last section answers it:
The entities in the first output are the attribute layer escaping on top; the browser decodes them back to quotes before Alpine ever sees the string, so the two passes compose without either weakening the other. Getting the position wrong is an error rather than a silent problem:
| js is a claim about context, and a declared slot is then passed through verbatim. A false claim turns the automatic escaper off and puts nothing in its place, so it is refused rather than ignored.

Omitting the filter

An undeclared slot in a JavaScript-evaluated attribute is not an error. The compiler escapes it for the position it found:
The first is a hostile value (');alert(1);(') neutralised inside the string it was written into. The other two are the ordinary numeric case. Leaving the filter off stays legal because comparing against a count or a loop index is a normal thing to write and cannot be proved safe or unsafe statically. In development mode the renderer records an advisory on every undeclared slot, naming the attribute and the filter that would state the intent. It is silent in slurp build, which always renders in production mode.
| unsafe_js is the escape hatch for the opposite case: JavaScript the theme itself wrote, passed in as a prop and composed into a handler. Escaping that would corrupt working code. It means “this is code, not data”, so never route a value from outside the theme through it.

A style value loses its punctuation

There is no useful escape for CSS, so a slot interpolated into style, :style or x-bind:style has the structural characters removed: ; { } ( ) " ' \ @ * < > and NUL.
With path set to a);color:red;x:url(b:
: and / are kept, because neither can open a new declaration on its own and both are needed by the background:url(...) shape. The consequence for ordinary use is that a value carrying legitimate punctuation arrives mangled. A CSS custom property set from a schema color setting is fine; anything that needs a function call, a quoted font name or a semicolon belongs in a class instead. Development mode warns when the strip actually changed a value.
A <style> body is a different matter: it is a raw text element, so ${ } inside one is not interpolated at all and is emitted as literal characters. Block tags do not run there either. Put dynamic values in a style attribute or in a custom property, not in a stylesheet body.

Two silent failures

JavaScript template literals in attributes

Slurp interpolates ${ } in attribute values itself, so this slot is evaluated against the server render context rather than the browser’s. If item is a client-side variable it resolves to nothing and the link renders as `/product/`, with no runtime error. The page looks built and carries dead links. This is diagnosed as a JsTemplateLiteralInAttribute warning, which slurp validate --warnings and slurp build -v print. It is a warning rather than an error because interpolating a genuine server value inside backticks is legal, and the compiler cannot tell the two apart.

A nested double quote ends the attribute

The lexer closes an attribute value at its first matching quote, before interpolation is considered, so the rest of the expression becomes stray attributes. Nothing is reported by the compiler. Use single quotes inside the expression, or the brace form, which is not quote-delimited:

What catches what

Next

How escaping works

Every escaping context, and the unescaped forms.

Security model

The threat model behind these rules, with the enforcing symbol named for each one.