Skip to main content
An expression is evaluated once, on the server, against a JSON context. It cannot call anything, cannot assign anything, and cannot fail: a missing value is null and renders as the empty string.

Interpolation forms

${ expr } and { expr } are the same node in text position. The brace form is shorter; the dollar form is unambiguous, because a bare { in text starts an expression:
To emit a literal brace, interpolate one: ${ "{" }.
Two raw-text contexts change the rules.In a <script> body a bare { is literal text, so ordinary JavaScript objects are safe to write, but ${ } is still interpolated and must carry | js or | json. Block tags such as {if} still work there.In a <style> body nothing is interpolated at all. ${ name } inside <style> is emitted verbatim, characters and all.

Precedence

Loosest first. All binary operators are left-associative, including the comparisons, so a < b < c parses as (a < b) < c.
Filters bind loosest of all. ${ ok ? "yes" : "no" | upper } applies upper to the whole ternary result, so with ok = true it renders YES, not yes. Parenthesise if you meant otherwise.A filter also cannot be followed by a binary operator. ${ a | upper + b } is a parse error, not a precedence surprise. Parenthesise: ${ (a | upper) + b }.

Operator semantics

Any arithmetic result that is NaN or infinite silently becomes 0.

Arithmetic coerces, comparison does not

Coerce explicitly with | int or | float whenever a value might arrive as a string. Money always does; a server decimal serialises to JSON as "49.990000".

Truthiness

Literals

Pass collections and objects through the render context.

Operators that do not exist

=== !== ?? ?. in ** typeof instanceof, every bitwise operator, every assignment operator, and arrow functions. Each is a parse or lex error, not a silent no-op. There is no ?. because none is needed: . is always null-safe. For a null fallback use | default(...) rather than ??.

Paths

Navigation is null-safe at every depth, so a missing intermediate yields the empty string rather than an error. A dynamic index works, including when the index is an {each} loop variable. With items = ["a","b","c"] and i = 1, ${ items[i] } renders b. After a . only an identifier is accepted, so a.0 is a parse error and a[0] is the only form.
There are no array properties or methods. items.length resolves to null and renders empty, with no diagnostic. Pass a count through the render context, or use loop.count inside the loop.

Reactive paths

$name marks a path as reactive for the optional browser runtime. Server-side it resolves exactly like a plain path, so ${ $cart.total } and ${ cart.total } render identically.

Function calls

f(a, b) PARSES and ALWAYS evaluates to null. There are no built-in callable functions of any kind, no way to register one, and no diagnostic.
The only call-shaped construct in the language is a filter. Compute the value on the server and pass it through the context, or reach for a filter.

Scoping

The page context is the root scope. {each} and {fetch} create child scopes, and a child shadows its parent. Components and layouts render in their OWN root scope: the page globals, overlaid by whatever was passed explicitly, with the explicit values winning on collision. So a component CAN read a page global it was never passed.
What a component cannot see is the caller’s LOCAL bindings. An {each} loop variable and loop.index are invisible inside a component unless passed as props, and the failure is silent: they render as empty values.

Escaping by context

An interpolation is escaped for exactly where it lands. The compiler selects the escaper from the position. A style slot losing characters is reported as a warning in development mode only. A production render strips silently.