Skip to main content
This page is a detailed companion to the Expressions guide. It covers every operator, pipe, and special variable available in {{ }} expressions and their Rx equivalents, with enough context to use each one confidently. Every entry shows both the Python Rx syntax and the equivalent {{ }} protocol syntax. They compile to the same thing; choose whichever fits your context.

Operators

Operators let expressions compute values from state rather than just reading them. They follow standard mathematical precedence: multiplication and division before addition and subtraction, with parentheses for explicit grouping.

Arithmetic

PythonProtocolDescription
count + 1{{ count + 1 }}Addition
total - discount{{ total - discount }}Subtraction
price * quantity{{ price * quantity }}Multiplication
amount / 2{{ amount / 2 }}Division
-score{{ -score }}Negation
Arithmetic expressions can appear anywhere a value is expected: in a Text component, in a SetState action value, or in a prop like Progress(value=...).

Comparison

PythonProtocolMeaning
count > 0{{ count > 0 }}Greater than
count >= 10{{ count >= 10 }}Greater than or equal
count < 100{{ count < 100 }}Less than
count <= 50{{ count <= 50 }}Less than or equal
status == 'active'{{ status == 'active' }}Equal (loose)
status != 'done'{{ status != 'done' }}Not equal

Logical

PythonProtocolMeaning
a & b{{ a && b }}AND
a | b{{ a || b }}OR
~a{{ !a }}NOT
The protocol also accepts and, or, not as keyword alternatives. Both && and || short-circuit. || doubles as a falsy default: {{ name || 'Anonymous' }}.
Bitwise & and | bind tighter than comparisons in Python. Always parenthesize: (score > 0) & (score < 100).

Ternary

PythonProtocol
active.then("On", "Off"){{ active ? 'On' : 'Off' }}
(score > 90).then("Pass", "Fail"){{ score > 90 ? 'Pass' : 'Fail' }}

String Concatenation

F-strings are preferred: f"Hello, {name}!" compiles to Hello, {{ name }}! The + operator concatenates when either operand is a string: {{ 'Hello, ' + name }}.

Pipes

Pipes transform expression values for display. A raw number like 2847500 becomes $2,847,500.00 with the currency pipe; an ISO date string becomes “March 15, 2025” with date:long. In Python, pipes are method calls on Rx objects; in the protocol, they chain with |. Pipes process values left to right. In {{ todos | rejectattr:done | length }}, the array is first filtered, then counted. You can chain as many pipes as needed.

Number Formatting

PipePythonProtocolResult
currency.currency() / .currency("EUR"){{ x | currency }} / {{ x | currency:EUR }}$1,234.00 / €1,234.00
percent.percent() / .percent(1){{ x | percent }} / {{ x | percent:1 }}76% / 75.6%
number.number() / .number(2){{ x | number }} / {{ x | number:2 }}1,234 / 1,234.00
round.round(2){{ x | round:2 }}3.14
abs.abs(){{ x | abs }}42
percent multiplies by 100 before formatting. A value of 0.756 becomes 75.6%, not 0.756%. number and currency produce locale-formatted output (en-US by default).

Date and Time

PipeArgumentExample Output
dateshort1/15/2025
date(default: medium)Jan 15, 2025
datelongJanuary 15, 2025
time2:30 PM
datetimeJan 15, 2025, 2:30 PM
Input must be an ISO date string. The time pipe also accepts time-only strings like "14:30".

String Formatting

PipeArgumentDescription
upperUppercase the string
lowerLowercase the string
truncatemax lengthClamp to N characters, append ... if truncated
pluralizesingular word (default item)Returns the word as-is for count 1, appends s otherwise
pluralize pairs well with length for labeling dynamic counts: Rx("items").length().pluralize("item") produces {{ items | length | pluralize:'item' }}, rendering as “1 item” or “3 items”.

Array Operations

PipeArgumentDescription
lengthNumber of elements (also works on strings)
joinseparator (default , )Join elements into a string
firstFirst element
lastLast element
selectattrattribute nameKeep items where the attribute is truthy
rejectattrattribute nameRemove items where the attribute is truthy

Chaining

Pipes chain left to right; each pipe receives the output of the one before it:
PythonProtocol
name.lower().truncate(20){{ name | lower | truncate:20 }}
todos.rejectattr("done").length(){{ todos | rejectattr:'done' | length }}
revenue.currency().upper(){{ revenue | currency | upper }}

Default Values

A bare literal after | acts as a default when the left side is null or undefined. In Python, use .default():
PythonProtocolBehavior
name.default("Anonymous"){{ name | 'Anonymous' }}"Anonymous" if name is undefined
count.default(0){{ count | 0 }}0 if count is undefined
This checks specifically for null/undefined: an empty string "" or 0 will not trigger the default. For broader falsy defaults (including empty strings, zero, and false), use || instead: {{ name || 'Anonymous' }}. Unknown pipe names pass the value through unchanged; no error, just unformatted output.

Context & Variables

Local Scope (let)

The let prop on any container introduces scoped bindings visible to its children only: The first Text renders “Don’t Panic, Arthur”. The second renders “Don’t Panic, Ford”: the inner let shadows name with a new value, but greeting is inherited unchanged. Use set_initial_state() for mutable data the user can change. Use let for fixed data passed into a section: labels, configuration, computed values. let bindings are read-only.

Capturing Loop Variables

When nesting ForEach loops, both define $index. Capture the outer index with let before entering the inner loop:
from prefab_ui.components import Text
from prefab_ui.components.control_flow import ForEach

with ForEach("groups", let={"gi": "{{ $index }}"}):
    with ForEach("groups.{{ gi }}.todos"):
        Text("Group {{ gi }}, item {{ $index }}")

Special Variables

These are runtime values injected by the framework at specific points in the component tree. They don’t exist in global state; access them with {{ }} template strings.

$event

Available inside action handlers. Contains the value emitted by the interaction:
Component$event value
Input / TextareaCurrent text (string)
SliderCurrent position (number)
Checkbox / SwitchChecked state (boolean)
SelectSelected value (string)
RadioGroupSelected value (string)
Buttonundefined
To capture $event explicitly, pass EVENT as the value: SetState("last_volume", EVENT). Form controls update their own state key automatically, so this is mainly useful when writing the event value to a different key.

$error

Available inside on_error callbacks. Contains the error message from the failed action:
CallTool(
    "save_data",
    on_error=ShowToast("Failed: {{ $error }}", variant="error"),
)

$index

Available inside ForEach. The zero-based index of the current item. Essential for actions targeting a specific row: SetState("todos.{{ $index }}.done").

$item

Available inside ForEach. The entire current item object. Individual fields are accessible directly as {{ name }}, but $item is useful for passing the whole object to an action:
with ForEach("users"):
    Text("{{ name }}")
    Button("Edit", on_click=CallTool("edit_user", arguments={"user": "{{ $item }}"}))

Dot Paths

Attribute access on Rx builds dot paths: Rx("user").address.city compiles to {{ user.address.city }}. Integer segments address array items: {{ todos.0.done }}. .length works on arrays and strings. If any segment is null or undefined, the expression resolves to undefined rather than throwing.

Undefined Values

For sole-value templates ("{{ missing }}"): returns the literal template string unchanged, which helps with debugging. For mixed templates ("Hi {{ missing }}!"): undefined resolves to an empty string, producing "Hi !". Use the default pipe for fallbacks: {{ missing | 'N/A' }}.
Boolean prop gotcha. disabled="{{ waiting }}" with undefined waiting evaluates to the string {{ waiting }}, which is truthy. Fix with: disabled="{{ waiting | false }}".

Grammar

The full BNF for the expression language inside {{ }} delimiters. The Rx DSL handles expression construction for you, but this is useful for writing protocol JSON directly or building tooling.
expr       -> pipe
pipe       -> ternary ( '|' ( ident ( ':' arg )? | literal ) )*
ternary    -> or ( '?' expr ':' expr )?
or         -> and ( '||' and )*
and        -> not ( '&&' not )*
not        -> '!' not | comp
comp       -> add ( ( '==' | '!=' | '>' | '<' | '>=' | '<=' ) add )?
add        -> mul ( ( '+' | '-' ) mul )*
mul        -> unary ( ( '*' | '/' ) unary )*
unary      -> ( '-' | '+' ) unary | primary
primary    -> '(' expr ')' | number | string | 'true' | 'false' | 'null' | ident
ident      -> name ( '.' name )*
Pipe has the lowest precedence, so price * quantity | currency parses as (price * quantity) | currency. The ternary operator is next-lowest, meaning a > 0 ? a : -a | abs parses as (a > 0 ? a : -a) | abs. Use parentheses to override. Keywords and, or, and not are interchangeable with &&, ||, and !. Strings use single quotes inside expressions: {{ status == 'active' }}.