---
title: Expression Reference
sidebarTitle: Expression Reference
description: Complete reference for operators, formatting pipes, context variables, and expression grammar.
icon: book-open
---

import { ComponentPreview } from '/snippets/component-preview.mdx'

This page is a detailed companion to the [Expressions](/guides/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

| Python | Protocol | Description |
|--------|----------|-------------|
| `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

| Python | Protocol | Meaning |
|--------|----------|---------|
| `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

| Python | Protocol | Meaning |
|--------|----------|---------|
| `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' }}`.

<Warning>
Bitwise `&` and `|` bind tighter than comparisons in Python. Always parenthesize: `(score > 0) & (score < 100)`.
</Warning>

### Ternary

| Python | Protocol |
|--------|----------|
| `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

<ComponentPreview json={{"view":{"type":"Card","children":[{"type":"CardHeader","children":[{"type":"CardTitle","content":"Q4 Revenue"},{"type":"CardDescription","content":"{{ revenue | currency }}"}]},{"type":"CardContent","children":[{"cssClass":"gap-2","type":"Column","children":[{"type":"Progress","value":"{{ revenue / target * 100 }}","min":0,"max":100.0,"variant":"default"},{"cssClass":"gap-2 items-center","type":"Row","children":[{"content":"{{ revenue / target | percent:1 }} of target","type":"Text"},{"type":"Badge","label":"{{ growth | percent:1 }} YoY","variant":"secondary"}]}]}]}]},"state":{"revenue":2847500,"target":3000000,"growth":0.124}}} playground=" ">
<CodeGroup>
```python Python icon="python"
from prefab_ui.app import set_initial_state
from prefab_ui.components import (
    Badge, Card, CardContent, CardDescription,
    CardHeader, CardTitle, Column, Progress, Row, Text,
)

state = set_initial_state(revenue=2847500, target=3000000, growth=0.124)

with Card():
    with CardHeader():
        CardTitle("Q4 Revenue")
        CardDescription(state.revenue.currency())
    with CardContent():
        with Column(gap=2):
            Progress(value=(state.revenue / state.target * 100), max=100)
            with Row(gap=2, align="center"):
                Text(f"{(state.revenue / state.target).percent(1)} of target")
                Badge(f"{state.growth.percent(1)} YoY", variant="secondary")
```
```json Protocol icon="brackets-curly"
{
  "view": {
    "type": "Card",
    "children": [
      {
        "type": "CardHeader",
        "children": [
          {"type": "CardTitle", "content": "Q4 Revenue"},
          {"type": "CardDescription", "content": "{{ revenue | currency }}"}
        ]
      },
      {
        "type": "CardContent",
        "children": [
          {
            "cssClass": "gap-2",
            "type": "Column",
            "children": [
              {
                "type": "Progress",
                "value": "{{ revenue / target * 100 }}",
                "min": 0,
                "max": 100.0,
                "variant": "default"
              },
              {
                "cssClass": "gap-2 items-center",
                "type": "Row",
                "children": [
                  {"content": "{{ revenue / target | percent:1 }} of target", "type": "Text"},
                  {
                    "type": "Badge",
                    "label": "{{ growth | percent:1 }} YoY",
                    "variant": "secondary"
                  }
                ]
              }
            ]
          }
        ]
      }
    ]
  },
  "state": {"revenue": 2847500, "target": 3000000, "growth": 0.124}
}
```
</CodeGroup>
</ComponentPreview>

| Pipe | Python | Protocol | Result |
|------|--------|----------|--------|
| `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

<ComponentPreview json={{"view":{"type":"Card","children":[{"type":"CardHeader","children":[{"type":"CardTitle","content":"Team Standup"},{"type":"CardDescription","content":"{{ starts | date:long }}"}]},{"type":"CardContent","children":[{"cssClass":"gap-2 items-center","type":"Row","children":[{"type":"Badge","label":"{{ starts | time }}","variant":"outline"},{"content":"{{ starts | datetime }}","type":"Text"}]}]}]},"state":{"starts":"2025-01-15T14:30:00Z"}}} playground=" ">
<CodeGroup>
```python Python icon="python"
from prefab_ui.app import set_initial_state
from prefab_ui.components import (
    Badge, Card, CardContent, CardDescription,
    CardHeader, CardTitle, Row, Text,
)

state = set_initial_state(starts="2025-01-15T14:30:00Z")

with Card():
    with CardHeader():
        CardTitle("Team Standup")
        CardDescription(state.starts.date("long"))
    with CardContent():
        with Row(gap=2, align="center"):
            Badge(state.starts.time(), variant="outline")
            Text(state.starts.datetime())
```
```json Protocol icon="brackets-curly"
{
  "view": {
    "type": "Card",
    "children": [
      {
        "type": "CardHeader",
        "children": [
          {"type": "CardTitle", "content": "Team Standup"},
          {"type": "CardDescription", "content": "{{ starts | date:long }}"}
        ]
      },
      {
        "type": "CardContent",
        "children": [
          {
            "cssClass": "gap-2 items-center",
            "type": "Row",
            "children": [
              {"type": "Badge", "label": "{{ starts | time }}", "variant": "outline"},
              {"content": "{{ starts | datetime }}", "type": "Text"}
            ]
          }
        ]
      }
    ]
  },
  "state": {"starts": "2025-01-15T14:30:00Z"}
}
```
</CodeGroup>
</ComponentPreview>

| Pipe | Argument | Example Output |
|------|----------|---------------|
| `date` | `short` | `1/15/2025` |
| `date` | (default: medium) | `Jan 15, 2025` |
| `date` | `long` | `January 15, 2025` |
| `time` | | `2:30 PM` |
| `datetime` | | `Jan 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

<ComponentPreview json={{"view":{"type":"Card","children":[{"type":"CardHeader","children":[{"type":"CardTitle","content":"{{ name | upper }}"}]},{"type":"CardContent","children":[{"cssClass":"gap-2","type":"Column","children":[{"type":"Badge","label":"{{ name | lower }}","variant":"secondary"},{"content":"{{ bio | truncate:80 }}","type":"Text"}]}]}]},"state":{"name":"Arthur Dent","bio":"A perfectly ordinary man who woke up one Thursday to discover his house was about to be demolished, his planet destroyed, and his best friend was an alien from Betelgeuse."}}} playground=" ">
<CodeGroup>
```python Python icon="python"
from prefab_ui.app import set_initial_state
from prefab_ui.components import (
    Badge, Card, CardContent, CardHeader, CardTitle, Column, Text,
)

state = set_initial_state(
    name="Arthur Dent",
    bio="A perfectly ordinary man who woke up one Thursday to discover his house"
    " was about to be demolished, his planet destroyed, and his best friend"
    " was an alien from Betelgeuse.",
)

with Card():
    with CardHeader():
        CardTitle(state.name.upper())
    with CardContent():
        with Column(gap=2):
            Badge(state.name.lower(), variant="secondary")
            Text(state.bio.truncate(80))
```
```json Protocol icon="brackets-curly"
{
  "view": {
    "type": "Card",
    "children": [
      {
        "type": "CardHeader",
        "children": [{"type": "CardTitle", "content": "{{ name | upper }}"}]
      },
      {
        "type": "CardContent",
        "children": [
          {
            "cssClass": "gap-2",
            "type": "Column",
            "children": [
              {"type": "Badge", "label": "{{ name | lower }}", "variant": "secondary"},
              {"content": "{{ bio | truncate:80 }}", "type": "Text"}
            ]
          }
        ]
      }
    ]
  },
  "state": {
    "name": "Arthur Dent",
    "bio": "A perfectly ordinary man who woke up one Thursday to discover his house was about to be demolished, his planet destroyed, and his best friend was an alien from Betelgeuse."
  }
}
```
</CodeGroup>
</ComponentPreview>

| Pipe | Argument | Description |
|------|----------|-------------|
| `upper` | | Uppercase the string |
| `lower` | | Lowercase the string |
| `truncate` | max length | Clamp to N characters, append `...` if truncated |
| `pluralize` | singular 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

<ComponentPreview json={{"view":{"cssClass":"gap-2","type":"Column","children":[{"cssClass":"gap-2 items-center","type":"Row","children":[{"content":"{{ todos | rejectattr:done | length }} remaining","type":"Text"},{"type":"Badge","label":"{{ todos | length }} total","variant":"outline"}]},{"content":"First: {{ todos | selectattr:name | first }}","type":"Text"},{"content":"All: {{ todos | selectattr:name | join:', ' }}","type":"Text"}]},"state":{"todos":[{"name":"Learn Prefab","done":true},{"name":"Build dashboard","done":false},{"name":"Deploy to production","done":false}]}}} playground=" ">
<CodeGroup>
```python Python icon="python"
from prefab_ui.app import set_initial_state
from prefab_ui.components import Badge, Column, Row, Text

state = set_initial_state(
    todos=[
        {"name": "Learn Prefab", "done": True},
        {"name": "Build dashboard", "done": False},
        {"name": "Deploy to production", "done": False},
    ]
)

with Column(gap=2):
    with Row(gap=2, align="center"):
        Text(f"{state.todos.rejectattr('done').length()} remaining")
        Badge(f"{state.todos.length()} total", variant="outline")
    Text(f"First: {state.todos.selectattr('name').first()}")
    Text(f"All: {state.todos.selectattr('name').join(', ')}")
```
```json Protocol icon="brackets-curly"
{
  "view": {
    "cssClass": "gap-2",
    "type": "Column",
    "children": [
      {
        "cssClass": "gap-2 items-center",
        "type": "Row",
        "children": [
          {"content": "{{ todos | rejectattr:done | length }} remaining", "type": "Text"},
          {"type": "Badge", "label": "{{ todos | length }} total", "variant": "outline"}
        ]
      },
      {"content": "First: {{ todos | selectattr:name | first }}", "type": "Text"},
      {"content": "All: {{ todos | selectattr:name | join:', ' }}", "type": "Text"}
    ]
  },
  "state": {
    "todos": [
      {"name": "Learn Prefab", "done": true},
      {"name": "Build dashboard", "done": false},
      {"name": "Deploy to production", "done": false}
    ]
  }
}
```
</CodeGroup>
</ComponentPreview>

| Pipe | Argument | Description |
|------|----------|-------------|
| `length` | | Number of elements (also works on strings) |
| `join` | separator (default `, `) | Join elements into a string |
| `first` | | First element |
| `last` | | Last element |
| `selectattr` | attribute name | Keep items where the attribute is truthy |
| `rejectattr` | attribute name | Remove items where the attribute is truthy |

### Chaining

Pipes chain left to right; each pipe receives the output of the one before it:

| Python | Protocol |
|--------|----------|
| `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()`:

| Python | Protocol | Behavior |
|--------|----------|----------|
| `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:

<ComponentPreview json={{"view":{"cssClass":"gap-2","let":{"greeting":"Don't Panic","name":"Arthur"},"type":"Column","children":[{"content":"{{ greeting }}, {{ name }}","type":"Text"},{"let":{"name":"Ford"},"type":"Column","children":[{"content":"{{ greeting }}, {{ name }}","type":"Text"}]}]}}} playground="ZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cyBpbXBvcnQgQ29sdW1uLCBUZXh0Cgp3aXRoIENvbHVtbihnYXA9MiwgbGV0PXsiZ3JlZXRpbmciOiAiRG9uJ3QgUGFuaWMiLCAibmFtZSI6ICJBcnRodXIifSk6CiAgICBUZXh0KCJ7eyBncmVldGluZyB9fSwge3sgbmFtZSB9fSIpCiAgICB3aXRoIENvbHVtbihsZXQ9eyJuYW1lIjogIkZvcmQifSk6CiAgICAgICAgVGV4dCgie3sgZ3JlZXRpbmcgfX0sIHt7IG5hbWUgfX0iKQo=">
<CodeGroup>
```python Python icon="python"
from prefab_ui.components import Column, Text

with Column(gap=2, let={"greeting": "Don't Panic", "name": "Arthur"}):
    Text("{{ greeting }}, {{ name }}")
    with Column(let={"name": "Ford"}):
        Text("{{ greeting }}, {{ name }}")
```
```json Protocol icon="brackets-curly"
{
  "view": {
    "cssClass": "gap-2",
    "let": {"greeting": "Don't Panic", "name": "Arthur"},
    "type": "Column",
    "children": [
      {"content": "{{ greeting }}, {{ name }}", "type": "Text"},
      {
        "let": {"name": "Ford"},
        "type": "Column",
        "children": [{"content": "{{ greeting }}, {{ name }}", "type": "Text"}]
      }
    ]
  }
}
```
</CodeGroup>
</ComponentPreview>

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](/components/foreach) loops, both define `$index`. Capture the outer index with `let` before entering the inner loop:

```python
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 / Textarea | Current text (string) |
| Slider | Current position (number) |
| Checkbox / Switch | Checked state (boolean) |
| Select | Selected value (string) |
| RadioGroup | Selected value (string) |
| Button | `undefined` |

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:

```python
CallTool(
    "save_data",
    on_error=ShowToast("Failed: {{ $error }}", variant="error"),
)
```

#### `$index`

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

#### `$item`

Available inside [ForEach](/components/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:

```python
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' }}`.

<Warning>
**Boolean prop gotcha.** `disabled="{{ waiting }}"` with undefined `waiting` evaluates to the *string* `{{ waiting }}`, which is truthy. Fix with: `disabled="{{ waiting | false }}"`.
</Warning>

## 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' }}`.
