> ## Documentation Index
> Fetch the complete documentation index at: https://prefab.prefect.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Expressions

export const ComponentPreview = ({json, height, resizable, bare, hideJson, playground, children}) => {
  const hostRef = React.useRef(null);
  const handleRef = React.useRef(null);
  const cardRef = React.useRef(null);
  const containerRef = React.useRef(null);
  const [previewWidth, setPreviewWidth] = React.useState(null);
  const [isDragging, setIsDragging] = React.useState(false);
  const [playgroundHref, setPlaygroundHref] = React.useState(null);
  React.useEffect(function () {
    if (!playground) return;
    var isLocal = location.hostname === "localhost" || location.hostname === "127.0.0.1";
    var path = isLocal ? "/playground" : "/docs/playground";
    var url = new URL(path, window.location.href);
    url.hash = "code=" + playground;
    setPlaygroundHref(url.href);
  }, [playground]);
  var jsonStr = typeof json === "string" ? json : JSON.stringify(json, null, 2);
  React.useEffect(function () {
    if (cardRef.current) {
      var card = cardRef.current.closest(".group");
      if (card) card.classList.remove("group");
    }
    var host = hostRef.current;
    if (!host || !json) return;
    function mount() {
      if (!window.__prefab || !host) return;
      var dark = document.documentElement.classList.contains("dark");
      handleRef.current = window.__prefab.mountPreview(host, jsonStr, {
        dark: dark
      });
      if (bare && host.shadowRoot) {
        var m = host.shadowRoot.querySelector("[data-prefab-mount]");
        if (m) m.style.background = "transparent";
      }
    }
    if (window.__prefab) {
      mount();
    } else {
      if (!window.__prefabLoading) {
        if (!window.__prefabReady) {
          var s = document.createElement("script");
          s.src = "/renderer.js";
          document.head.appendChild(s);
        }
        window.__prefabLoading = new Promise(function (resolve) {
          function check() {
            if (window.__prefabReady) {
              window.__prefabReady.then(resolve);
            } else {
              setTimeout(check, 10);
            }
          }
          check();
        });
      }
      window.__prefabLoading.then(mount);
    }
    var observer = new MutationObserver(function () {
      var dark = document.documentElement.classList.contains("dark");
      if (handleRef.current) handleRef.current.setDark(dark);
    });
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class"]
    });
    return function () {
      observer.disconnect();
      if (handleRef.current) {
        handleRef.current.unmount();
        handleRef.current = null;
      }
    };
  }, [json]);
  function startResize(e) {
    e.preventDefault();
    var startX = e.clientX;
    var startW = hostRef.current ? hostRef.current.offsetWidth : 0;
    var maxW = containerRef.current ? containerRef.current.offsetWidth - 10 : startW;
    setIsDragging(true);
    function onMove(ev) {
      var w = Math.max(260, Math.min(startW + (ev.clientX - startX), maxW));
      setPreviewWidth(w);
    }
    function onUp() {
      document.removeEventListener("mousemove", onMove);
      document.removeEventListener("mouseup", onUp);
      setIsDragging(false);
    }
    document.addEventListener("mousemove", onMove);
    document.addEventListener("mouseup", onUp);
  }
  if (bare) {
    return <div ref={hostRef} />;
  }
  if (resizable) {
    return <Card>
        <div ref={cardRef} />
        <div ref={containerRef} style={{
      display: "flex",
      position: "relative",
      overflow: "hidden"
    }}>
          <div ref={hostRef} style={{
      flex: previewWidth ? "none" : 1,
      width: previewWidth ? previewWidth + "px" : undefined,
      minWidth: 0
    }} />
          <div onMouseDown={startResize} style={{
      width: "14px",
      flexShrink: 0,
      cursor: "col-resize",
      background: isDragging ? "var(--border, #e5e7eb)" : "color-mix(in srgb, var(--border, #e5e7eb) 40%, transparent)",
      display: "flex",
      alignItems: "center",
      justifyContent: "center",
      transition: "background 0.15s",
      userSelect: "none"
    }}>
            <svg width="6" height="24" style={{
      opacity: 0.5
    }}>
              <circle cx="2" cy="4" r="1.2" fill="currentColor" />
              <circle cx="2" cy="9" r="1.2" fill="currentColor" />
              <circle cx="2" cy="14" r="1.2" fill="currentColor" />
              <circle cx="2" cy="19" r="1.2" fill="currentColor" />
              <circle cx="5" cy="4" r="1.2" fill="currentColor" />
              <circle cx="5" cy="9" r="1.2" fill="currentColor" />
              <circle cx="5" cy="14" r="1.2" fill="currentColor" />
              <circle cx="5" cy="19" r="1.2" fill="currentColor" />
            </svg>
          </div>
          <div style={{
      flex: previewWidth ? 1 : "0 0 24px",
      minWidth: "24px",
      background: "repeating-linear-gradient(-45deg, transparent, transparent 3px, var(--border, #e5e7eb) 3px, var(--border, #e5e7eb) 4px)",
      opacity: 0.4
    }} />
          {previewWidth && <div style={{
      position: "absolute",
      bottom: "8px",
      right: "8px",
      background: "var(--background, white)",
      border: "1px solid var(--border, #e5e7eb)",
      borderRadius: "4px",
      padding: "1px 6px",
      fontSize: "11px",
      fontFamily: "monospace",
      color: "var(--muted-foreground, #6b7280)",
      pointerEvents: "none"
    }}>
              {previewWidth}px
            </div>}
        </div>
        {playgroundHref && <div style={{
      display: "flex",
      justifyContent: "flex-end",
      padding: "4px 0 8px"
    }}>
            <a href={playgroundHref} target="_blank" rel="noopener noreferrer" style={{
      fontSize: "11px",
      opacity: 0.4,
      textDecoration: "none",
      textDecorationLine: "none",
      borderBottom: "none",
      boxShadow: "none",
      display: "inline-flex",
      alignItems: "center",
      gap: "3px",
      transition: "opacity 0.15s",
      fontWeight: 400
    }} onMouseOver={function (e) {
      e.currentTarget.style.opacity = "0.6";
    }} onMouseOut={function (e) {
      e.currentTarget.style.opacity = "0.4";
    }}>
              Edit in Playground
              <svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M15 3h6v6" /><path d="M10 14 21 3" /><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></svg>
            </a>
          </div>}
        <div style={{
      marginBottom: "-2rem"
    }}>{children}</div>
      </Card>;
  }
  return <Card>
      <div ref={cardRef} />
      <div ref={hostRef} />
      {playgroundHref && <div style={{
    display: "flex",
    justifyContent: "flex-end",
    padding: "4px 0 8px"
  }}>
          <a href={playgroundHref} target="_blank" rel="noopener noreferrer" style={{
    fontSize: "11px",
    opacity: 0.4,
    textDecoration: "none",
    textDecorationLine: "none",
    borderBottom: "none",
    boxShadow: "none",
    display: "inline-flex",
    alignItems: "center",
    gap: "3px",
    transition: "opacity 0.15s",
    fontWeight: 400
  }} onMouseOver={function (e) {
    e.currentTarget.style.opacity = "0.6";
  }} onMouseOut={function (e) {
    e.currentTarget.style.opacity = "0.4";
  }}>
            Edit in Playground
            <svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M15 3h6v6" /><path d="M10 14 21 3" /><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></svg>
          </a>
        </div>}
      <div style={{
    marginBottom: "-2rem"
  }}>{children}</div>
    </Card>;
};

Expressions are how components read and compute from state. Every `{{ }}` you write is a live binding: the renderer evaluates it against current state, tracks which keys it depends on, and re-evaluates whenever any of them change. The display stays current automatically, so you write the expression once and the renderer handles the rest.

The expression language is intentionally bounded. Prefab UIs are designed to be safely serializable, so the expression grammar covers arithmetic, comparisons, boolean logic, conditionals, and formatting. That's enough to drive any display while keeping business logic where it belongs: in your Python code or in a [server action](/concepts/actions).

## Template Expressions

The `{{ }}` syntax is the protocol-level unit of reactivity. Any string prop on any component can contain `{{ }}` expressions, and the renderer treats each one as a live dependency.

<ComponentPreview json={{"view":{"cssClass":"gap-3 items-center","type":"Row","children":[{"type":"Button","label":"-","variant":"outline","size":"default","disabled":false,"onClick":{"action":"setState","key":"count","value":"{{ count - 1 }}"}},{"type":"Badge","label":"{{ count }}","variant":"{{ count > 0 ? 'success' : 'destructive' }}"},{"type":"Button","label":"+","variant":"outline","size":"default","disabled":false,"onClick":{"action":"setState","key":"count","value":"{{ count + 1 }}"}}]}}} playground="ZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cyBpbXBvcnQgQnV0dG9uLCBSb3csIEJhZGdlCmZyb20gcHJlZmFiX3VpLmFjdGlvbnMgaW1wb3J0IFNldFN0YXRlCgoKd2l0aCBSb3coZ2FwPTMsIGFsaWduPSJjZW50ZXIiKToKICAgIEJ1dHRvbigiLSIsIHZhcmlhbnQ9Im91dGxpbmUiLCBvbl9jbGljaz1TZXRTdGF0ZSgiY291bnQiLCAie3sgY291bnQgLSAxIH19IikpCiAgICBCYWRnZSgie3sgY291bnQgfX0iLCB2YXJpYW50PSJ7eyBjb3VudCA-IDAgPyAnc3VjY2VzcycgOiAnZGVzdHJ1Y3RpdmUnIH19IikKICAgIEJ1dHRvbigiKyIsIHZhcmlhbnQ9Im91dGxpbmUiLCBvbl9jbGljaz1TZXRTdGF0ZSgiY291bnQiLCAie3sgY291bnQgKyAxIH19IikpCg">
  <CodeGroup>
    ```python Python icon="python" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    from prefab_ui.components import Button, Row, Badge
    from prefab_ui.actions import SetState


    with Row(gap=3, align="center"):
        Button("-", variant="outline", on_click=SetState("count", "{{ count - 1 }}"))
        Badge("{{ count }}", variant="{{ count > 0 ? 'success' : 'destructive' }}")
        Button("+", variant="outline", on_click=SetState("count", "{{ count + 1 }}"))
    ```

    ```json Protocol icon="brackets-curly" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    {
      "view": {
        "cssClass": "gap-3 items-center",
        "type": "Row",
        "children": [
          {
            "type": "Button",
            "label": "-",
            "variant": "outline",
            "size": "default",
            "disabled": false,
            "onClick": {"action": "setState", "key": "count", "value": "{{ count - 1 }}"}
          },
          {
            "type": "Badge",
            "label": "{{ count }}",
            "variant": "{{ count > 0 ? 'success' : 'destructive' }}"
          },
          {
            "type": "Button",
            "label": "+",
            "variant": "outline",
            "size": "default",
            "disabled": false,
            "onClick": {"action": "setState", "key": "count", "value": "{{ count + 1 }}"}
          }
        ]
      }
    }
    ```
  </CodeGroup>
</ComponentPreview>

`Text("{{ count }}")` displays whatever `count` holds and updates whenever it changes. The `SetState` actions write new values to `count`, and the text responds automatically. The expression `{{ count + 1 }}` computes before writing: the renderer evaluates it against current state each time. The reactivity is built in, so you write the binding once and the renderer keeps everything in sync.

Expressions can appear anywhere a string prop does. `Button(disabled="{{ loading }}")` disables a button based on state. `Badge(label="{{ items | length }}")` shows a count. `Progress(value="{{ percent }}")` drives a progress bar. The renderer re-evaluates each expression whenever any of its referenced keys change.

## The Rx Class

The expression language is accepted everywhere in the protocol, but writing `{{ }}` strings by hand in Python means giving up autocomplete, refactoring support, and typo detection. `Rx` lets you write the same expressions as Python code, with full editor support.

`Rx("key")` creates a reactive reference to a state key. The object it returns compiles to `{{ key }}` when serialized, but in your Python code it behaves like a regular value. You can do arithmetic on it, compare it, format it, and embed it in f-strings. Each operation returns a new `Rx` that represents the composed expression:

```python theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
from prefab_ui.rx import Rx

count = Rx("count")

Text(count)                # → Text("{{ count }}")
Text(count + 10)           # → Text("{{ count + 10 }}")
Text(f"Count: {count}")    # → Text("Count: {{ count }}")
```

Each operation on an `Rx` returns a new `Rx`, building an expression tree. At serialization time, that tree renders to the correct `{{ }}` protocol syntax. You work in Python; the protocol gets the right strings.

Method calls on `Rx` compile to **pipes** in the protocol, which format and transform values for display. The [Formatting with pipes](#formatting-with-pipes) section covers them in detail, but here's the basic idea:

```python theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
price = Rx("price")
quantity = Rx("quantity")

Text(f"Total: {(price * quantity).currency()}")
# → Text("Total: {{ price * quantity | currency }}")
```

F-strings work naturally with `Rx`. Each `Rx` reference inside an f-string becomes a separate `{{ }}` interpolation in the output. Surrounding text stays as literal strings:

```python theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
first = Rx("first")
last = Rx("last")

Text(f"Hello, {first} {last}!")
# → Text("Hello, {{ first }} {{ last }}!")
```

### The .rx Shorthand

Every interactive component manages a state key through its `name` prop, and that state can be read by any other component or action that knows the key name. The challenge is that key names are often auto-generated (like `slider_22`) or defined far from where you need them. Passing string key names around is fragile and hard to refactor.

The `.rx` property solves this. It returns `Rx(component.name)`, giving you a reactive reference to that component's state without needing to know or repeat the key name:

<ComponentPreview json={{"view":{"cssClass":"gap-3","type":"Column","children":[{"name":"slider_24","value":50.0,"type":"Slider","min":0.0,"max":100.0,"disabled":false,"size":"default"},{"content":"Volume: {{ slider_24 }}%","type":"Text"},{"type":"Progress","value":"{{ slider_24 }}","variant":"default","size":"default"}]},"state":{"slider_24":50.0}}} playground="ZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cyBpbXBvcnQgQ29sdW1uLCBQcm9ncmVzcywgU2xpZGVyLCBUZXh0Cgp3aXRoIENvbHVtbihnYXA9Myk6CiAgICBzbGlkZXIgPSBTbGlkZXIodmFsdWU9NTAsIG1pbj0wLCBtYXg9MTAwKQogICAgVGV4dChmIlZvbHVtZToge3NsaWRlci5yeH0lIikKICAgIFByb2dyZXNzKHZhbHVlPXNsaWRlci5yeCkK">
  <CodeGroup>
    ```python Python icon="python" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    from prefab_ui.components import Column, Progress, Slider, Text

    with Column(gap=3):
        slider = Slider(value=50, min=0, max=100)
        Text(f"Volume: {slider.rx}%")
        Progress(value=slider.rx)
    ```

    ```json Protocol icon="brackets-curly" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    {
      "view": {
        "cssClass": "gap-3",
        "type": "Column",
        "children": [
          {
            "name": "slider_24",
            "value": 50.0,
            "type": "Slider",
            "min": 0.0,
            "max": 100.0,
            "disabled": false,
            "size": "default"
          },
          {"content": "Volume: {{ slider_24 }}%", "type": "Text"},
          {
            "type": "Progress",
            "value": "{{ slider_24 }}",
            "variant": "default",
            "size": "default"
          }
        ]
      },
      "state": {"slider_24": 50.0}
    }
    ```
  </CodeGroup>
</ComponentPreview>

Drag the slider and the text and progress bar update together. `slider.rx` compiles to `{{ slider_22 }}` (the auto-generated name), but you never need to know or type that key. The dependency is expressed through the Python variable, which keeps your code readable and refactor-safe.

Because `.rx` returns an `Rx` object, all the same operations work on it. `slider.rx.number()` formats the value. `slider.rx > 50` produces a boolean expression. `slider.rx.then("High", "Low")` creates a conditional.

Use `.rx` when you have a component reference in scope. Use `Rx("key")` when working with keys from `PrefabApp(state={...})`, `ForEach` iteration, or state keys that belong to components you don't have a reference to.

Sometimes you need a component's `.rx` before the component exists — a label above a slider that shows its current value, for instance. `Rx(lambda: slider)` defers resolution until render time, when the variable is available. See [Forward References](/concepts/components#forward-references) for details.

## Operators

`Rx` overloads Python's operators so arithmetic, comparisons, and logic compile naturally to their protocol equivalents. Each operation returns a new `Rx` representing the composed expression. Operators 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=...)`.

<ComponentPreview json={{"view":{"cssClass":"gap-3","type":"Column","children":[{"cssClass":"gap-2 items-center","type":"Row","children":[{"content":"Quantity:","type":"Text"},{"type":"Button","label":"-","variant":"outline","size":"default","disabled":false,"onClick":{"action":"setState","key":"quantity","value":"{{ quantity - 1 }}"}},{"content":"{{ quantity }}","type":"Text"},{"type":"Button","label":"+","variant":"outline","size":"default","disabled":false,"onClick":{"action":"setState","key":"quantity","value":"{{ quantity + 1 }}"}}]},{"content":"Total: {{ price * quantity | currency }}","type":"Text"}]}}} playground="ZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cyBpbXBvcnQgU1RBVEUgYXMgc3RhdGUsIEJ1dHRvbiwgQ29sdW1uLCBSb3csIFRleHQKZnJvbSBwcmVmYWJfdWkuYWN0aW9ucyBpbXBvcnQgU2V0U3RhdGUKCgp3aXRoIENvbHVtbihnYXA9Myk6CiAgICB3aXRoIFJvdyhnYXA9MiwgYWxpZ249ImNlbnRlciIpOgogICAgICAgIFRleHQoIlF1YW50aXR5OiIpCiAgICAgICAgQnV0dG9uKCItIiwgdmFyaWFudD0ib3V0bGluZSIsIG9uX2NsaWNrPVNldFN0YXRlKCJxdWFudGl0eSIsIHN0YXRlLnF1YW50aXR5IC0gMSkpCiAgICAgICAgVGV4dChzdGF0ZS5xdWFudGl0eSkKICAgICAgICBCdXR0b24oIisiLCB2YXJpYW50PSJvdXRsaW5lIiwgb25fY2xpY2s9U2V0U3RhdGUoInF1YW50aXR5Iiwgc3RhdGUucXVhbnRpdHkgKyAxKSkKICAgIFRleHQoZiJUb3RhbDogeyhzdGF0ZS5wcmljZSAqIHN0YXRlLnF1YW50aXR5KS5jdXJyZW5jeSgpfSIpCg">
  <CodeGroup>
    ```python Python icon="python" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    from prefab_ui.components import STATE as state, Button, Column, Row, Text
    from prefab_ui.actions import SetState


    with Column(gap=3):
        with Row(gap=2, align="center"):
            Text("Quantity:")
            Button("-", variant="outline", on_click=SetState("quantity", state.quantity - 1))
            Text(state.quantity)
            Button("+", variant="outline", on_click=SetState("quantity", state.quantity + 1))
        Text(f"Total: {(state.price * state.quantity).currency()}")
    ```

    ```json Protocol icon="brackets-curly" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    {
      "view": {
        "cssClass": "gap-3",
        "type": "Column",
        "children": [
          {
            "cssClass": "gap-2 items-center",
            "type": "Row",
            "children": [
              {"content": "Quantity:", "type": "Text"},
              {
                "type": "Button",
                "label": "-",
                "variant": "outline",
                "size": "default",
                "disabled": false,
                "onClick": {"action": "setState", "key": "quantity", "value": "{{ quantity - 1 }}"}
              },
              {"content": "{{ quantity }}", "type": "Text"},
              {
                "type": "Button",
                "label": "+",
                "variant": "outline",
                "size": "default",
                "disabled": false,
                "onClick": {"action": "setState", "key": "quantity", "value": "{{ quantity + 1 }}"}
              }
            ]
          },
          {"content": "Total: {{ price * quantity | currency }}", "type": "Text"}
        ]
      }
    }
    ```
  </CodeGroup>
</ComponentPreview>

`price * quantity` compiles to `{{ price * quantity }}`, and `.currency()` formats the result for display.

### String Concatenation

F-strings are the cleanest way to mix reactive values with literal text. Each `Rx` reference becomes a separate `{{ }}` interpolation:

```python theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
first = Rx("first")
last = Rx("last")

Text(f"Hello, {first} {last}!")
# → Text("Hello, {{ first }} {{ last }}!")
```

The `+` operator also concatenates when either operand is a string: `{{ 'Hello, ' + name }}`. F-strings are almost always more readable.

### Comparisons

| 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             |

Comparisons return boolean expressions, which are the foundation for conditional rendering with [If/Elif/Else](/components/conditional) and conditional values with `.then()`:

```python theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
from prefab_ui.rx import Rx
from prefab_ui.components import Alert
from prefab_ui.components.control_flow import If, Elif

inventory = Rx("inventory")

with If(inventory == 0):
    Alert("Out of stock", variant="destructive")
with Elif((inventory > 0) & (inventory < 10)):
    Alert("Low stock")
```

The `If` component receives a boolean expression and only renders its children when the expression is true. When `inventory` changes, the conditions re-evaluate and the display updates.

### Logical Operators

| 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' }}` returns `'Anonymous'` when `name` is falsy (empty string, `false`, `0`, or undefined). For null/undefined-only defaults, use the `default` pipe instead (see [Default values](#default-values)).

<Warning>
  **Python precedence gotcha.** Bitwise `&` and `|` bind tighter than `>`, `<`, `==` in Python. Always wrap each comparison in parentheses:

  ```python theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
  # Correct
  (score > 0) & (score < 100)

  # Wrong: parsed as score > (0 & score) < 100
  score > 0 & score < 100
  ```

  This is a Python quirk. The `{{ }}` protocol expressions use standard precedence and don't require extra parentheses.
</Warning>

### Ternary

| Python                              | Protocol                             |
| ----------------------------------- | ------------------------------------ |
| `active.then("On", "Off")`          | `{{ active ? 'On' : 'Off' }}`        |
| `(score > 90).then("Pass", "Fail")` | `{{ score > 90 ? 'Pass' : 'Fail' }}` |

`.then(if_true, if_false)` chooses between two values based on a boolean expression. It compiles to the ternary operator `? :` in the protocol:

<ComponentPreview json={{"view":{"cssClass":"gap-2","type":"Column","children":[{"cssClass":"gap-3 items-center","type":"Row","children":[{"name":"active","value":false,"type":"Switch","size":"default","disabled":false,"required":false},{"content":"{{ active ? 'Online' : 'Offline' }}","type":"Text"}]}]},"state":{"active":false}}} playground="ZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cyBpbXBvcnQgU1RBVEUgYXMgc3RhdGUsIENvbHVtbiwgUm93LCBTd2l0Y2gsIFRleHQKCgp3aXRoIENvbHVtbihnYXA9Mik6CiAgICB3aXRoIFJvdyhnYXA9MywgYWxpZ249ImNlbnRlciIpOgogICAgICAgIFN3aXRjaChuYW1lPSJhY3RpdmUiKQogICAgICAgIFRleHQoc3RhdGUuYWN0aXZlLnRoZW4oIk9ubGluZSIsICJPZmZsaW5lIikpCg">
  <CodeGroup>
    ```python Python icon="python" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    from prefab_ui.components import STATE as state, Column, Row, Switch, Text


    with Column(gap=2):
        with Row(gap=3, align="center"):
            Switch(name="active")
            Text(state.active.then("Online", "Offline"))
    ```

    ```json Protocol icon="brackets-curly" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    {
      "view": {
        "cssClass": "gap-2",
        "type": "Column",
        "children": [
          {
            "cssClass": "gap-3 items-center",
            "type": "Row",
            "children": [
              {
                "name": "active",
                "value": false,
                "type": "Switch",
                "size": "default",
                "disabled": false,
                "required": false
              },
              {"content": "{{ active ? 'Online' : 'Offline' }}", "type": "Text"}
            ]
          }
        ]
      },
      "state": {"active": false}
    }
    ```
  </CodeGroup>
</ComponentPreview>

Toggle the switch and the text updates instantly. `.then()` works on any boolean `Rx`, including comparison results: `(score >= 90).then("Pass", "Fail")`. For choices beyond a simple two-way branch, [If/Elif/Else](/components/conditional) is cleaner.

## Formatting with Pipes

Raw state values often need formatting before display. Pipes transform expression results: formatting numbers as currency, dates as readable strings, arrays into counts, and more. In the protocol, pipes use the `|` syntax: `{{ revenue | currency }}`. In Python, they're method calls on `Rx`: `revenue.currency()`.

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 }}","max":100.0,"variant":"default","size":"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"}]}]}]}]}}} playground="ZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cyBpbXBvcnQgKAogICAgU1RBVEUgYXMgc3RhdGUsIEJhZGdlLCBDYXJkLCBDYXJkQ29udGVudCwgQ2FyZERlc2NyaXB0aW9uLAogICAgQ2FyZEhlYWRlciwgQ2FyZFRpdGxlLCBDb2x1bW4sIFByb2dyZXNzLCBSb3csIFRleHQsCikKCgp3aXRoIENhcmQoKToKICAgIHdpdGggQ2FyZEhlYWRlcigpOgogICAgICAgIENhcmRUaXRsZSgiUTQgUmV2ZW51ZSIpCiAgICAgICAgQ2FyZERlc2NyaXB0aW9uKHN0YXRlLnJldmVudWUuY3VycmVuY3koKSkKICAgIHdpdGggQ2FyZENvbnRlbnQoKToKICAgICAgICB3aXRoIENvbHVtbihnYXA9Mik6CiAgICAgICAgICAgIFByb2dyZXNzKHZhbHVlPShzdGF0ZS5yZXZlbnVlIC8gc3RhdGUudGFyZ2V0ICogMTAwKSwgbWF4PTEwMCkKICAgICAgICAgICAgd2l0aCBSb3coZ2FwPTIsIGFsaWduPSJjZW50ZXIiKToKICAgICAgICAgICAgICAgIFRleHQoZiJ7KHN0YXRlLnJldmVudWUgLyBzdGF0ZS50YXJnZXQpLnBlcmNlbnQoMSl9IG9mIHRhcmdldCIpCiAgICAgICAgICAgICAgICBCYWRnZShmIntzdGF0ZS5ncm93dGgucGVyY2VudCgxKX0gWW9ZIiwgdmFyaWFudD0ic2Vjb25kYXJ5IikK">
  <CodeGroup>
    ```python Python icon="python" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    from prefab_ui.components import (
        STATE as state, Badge, Card, CardContent, CardDescription,
        CardHeader, CardTitle, Column, Progress, Row, Text,
    )


    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" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    {
      "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 }}",
                    "max": 100.0,
                    "variant": "default",
                    "size": "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"
                      }
                    ]
                  }
                ]
              }
            ]
          }
        ]
      }
    }
    ```
  </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`      |
| `compact`  | `.compact()` / `.compact(0)`       | `{{ x \| compact }}` / `{{ x \| compact:0 }}`     | `1.8M` / `2M`             |
| `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). `compact` uses compact notation to abbreviate large numbers: `1800000` becomes `1.8M`, `470000` becomes `470K`, and small values like `42` pass through unchanged. The optional decimals argument controls precision.

### 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"}]}]}]}}} playground="ZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cyBpbXBvcnQgKAogICAgU1RBVEUgYXMgc3RhdGUsIEJhZGdlLCBDYXJkLCBDYXJkQ29udGVudCwgQ2FyZERlc2NyaXB0aW9uLAogICAgQ2FyZEhlYWRlciwgQ2FyZFRpdGxlLCBSb3csIFRleHQsCikKCgp3aXRoIENhcmQoKToKICAgIHdpdGggQ2FyZEhlYWRlcigpOgogICAgICAgIENhcmRUaXRsZSgiVGVhbSBTdGFuZHVwIikKICAgICAgICBDYXJkRGVzY3JpcHRpb24oc3RhdGUuc3RhcnRzLmRhdGUoImxvbmciKSkKICAgIHdpdGggQ2FyZENvbnRlbnQoKToKICAgICAgICB3aXRoIFJvdyhnYXA9MiwgYWxpZ249ImNlbnRlciIpOgogICAgICAgICAgICBCYWRnZShzdGF0ZS5zdGFydHMudGltZSgpLCB2YXJpYW50PSJvdXRsaW5lIikKICAgICAgICAgICAgVGV4dChzdGF0ZS5zdGFydHMuZGF0ZXRpbWUoKSkK">
  <CodeGroup>
    ```python Python icon="python" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    from prefab_ui.components import (
        STATE as state, Badge, Card, CardContent, CardDescription,
        CardHeader, CardTitle, Row, Text,
    )


    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" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    {
      "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"}
                ]
              }
            ]
          }
        ]
      }
    }
    ```
  </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"}]}]}]}}} playground="ZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cyBpbXBvcnQgKAogICAgU1RBVEUgYXMgc3RhdGUsIEJhZGdlLCBDYXJkLCBDYXJkQ29udGVudCwgQ2FyZEhlYWRlciwgQ2FyZFRpdGxlLCBDb2x1bW4sIFRleHQsCikKCgp3aXRoIENhcmQoKToKICAgIHdpdGggQ2FyZEhlYWRlcigpOgogICAgICAgIENhcmRUaXRsZShzdGF0ZS5uYW1lLnVwcGVyKCkpCiAgICB3aXRoIENhcmRDb250ZW50KCk6CiAgICAgICAgd2l0aCBDb2x1bW4oZ2FwPTIpOgogICAgICAgICAgICBCYWRnZShzdGF0ZS5uYW1lLmxvd2VyKCksIHZhcmlhbnQ9InNlY29uZGFyeSIpCiAgICAgICAgICAgIFRleHQoc3RhdGUuYmlvLnRydW5jYXRlKDgwKSkK">
  <CodeGroup>
    ```python Python icon="python" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    from prefab_ui.components import (
        STATE as state, Badge, Card, CardContent, CardHeader, CardTitle, Column, Text,
    )


    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" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    {
      "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"}
                ]
              }
            ]
          }
        ]
      }
    }
    ```
  </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

Array pipes filter and summarize lists stored in state. Given a list of todo items, each with a `done` flag, you can count how many are complete, how many remain, and how many there are total — all reactively.

`selectattr("done")` keeps only items where `done` is truthy. `rejectattr("done")` keeps items where it's falsy. Chain `.length()` to count the filtered results. These pipes work together to turn a raw array into meaningful summary numbers.

<ComponentPreview json={{"view":{"cssClass":"gap-2","type":"Column","children":[{"content":"Total items: {{ todos | length }}","type":"Text"},{"content":"Done: {{ todos | selectattr:done | length }}","type":"Text"},{"content":"Remaining: {{ todos | rejectattr:done | length }}","type":"Text"}]}}} playground="ZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cyBpbXBvcnQgU1RBVEUgYXMgc3RhdGUsIEJhZGdlLCBDb2x1bW4sIFJvdywgVGV4dAoKCndpdGggQ29sdW1uKGdhcD0yKToKICAgIFRleHQoZiJUb3RhbCBpdGVtczoge3N0YXRlLnRvZG9zLmxlbmd0aCgpfSIpCiAgICBUZXh0KGYiRG9uZToge3N0YXRlLnRvZG9zLnNlbGVjdGF0dHIoJ2RvbmUnKS5sZW5ndGgoKX0iKQogICAgVGV4dChmIlJlbWFpbmluZzoge3N0YXRlLnRvZG9zLnJlamVjdGF0dHIoJ2RvbmUnKS5sZW5ndGgoKX0iKQo">
  <CodeGroup>
    ```python Python icon="python" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    from prefab_ui.components import STATE as state, Badge, Column, Row, Text


    with Column(gap=2):
        Text(f"Total items: {state.todos.length()}")
        Text(f"Done: {state.todos.selectattr('done').length()}")
        Text(f"Remaining: {state.todos.rejectattr('done').length()}")
    ```

    ```json Protocol icon="brackets-curly" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    {
      "view": {
        "cssClass": "gap-2",
        "type": "Column",
        "children": [
          {"content": "Total items: {{ todos | length }}", "type": "Text"},
          {"content": "Done: {{ todos | selectattr:done | length }}", "type": "Text"},
          {"content": "Remaining: {{ todos | rejectattr:done | length }}", "type": "Text"}
        ]
      }
    }
    ```
  </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          |

The `default` pipe 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 and Variables

### Local Scope

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" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    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" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    {
      "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 `PrefabApp(state={...})` for mutable data the user can change. Use `let` for fixed data passed into a section: labels, configuration, static 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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
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.

## Type Preservation

When an `Rx` expression is the *entire* value of a prop, the renderer resolves it to the appropriate JavaScript type. `Progress(value=slider.rx)` serializes to `"{{ slider_22 }}"`, and the renderer resolves it to the number `50`, preserving the type for props that expect numbers or booleans.

When an expression is embedded in surrounding text, the result is always a string. `f"Volume: {slider.rx}%"` resolves to `"Volume: 50%"` because the surrounding text forces string concatenation.

The practical rule: if a prop expects a number or boolean (`value`, `min`, `max`, `disabled`, `checked`), pass the `Rx` expression as the sole value. Wrapping it in an f-string converts the result to a string, which may cause unexpected behavior.

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