Dynamic Expressions
Data binding, conditional values, and string interpolation in Stream UI specs
Expressions are special objects in prop values that are resolved at render time. They connect your UI to state, repeat contexts, and computed logic. Instead of static values, any prop can contain an expression that dynamically resolves based on the current state.
Expression Types
Stream UI supports 8 expression types:
| Expression | Syntax | Direction | Description |
|---|---|---|---|
$state | { "$state": "/path" } | Read | Read a value from global state |
$item | { "$item": "field" } | Read | Read a field from the current repeat item |
$index | { "$index": true } | Read | Current repeat index (zero-based) |
$bindState | { "$bindState": "/path" } | Read + Write | Two-way binding to a state path |
$bindItem | { "$bindItem": "field" } | Read + Write | Two-way binding to a repeat item field |
$cond | { "$cond": ..., "$then": ..., "$else": ... } | Read | Conditional value selection |
$computed | { "$computed": "fnName", "args": {} } | Read | Call a registered function |
$template | { "$template": "Hello ${/name}!" } | Read | String interpolation |
$state — Read from State
Read a value from the global state model using a JSON Pointer path:
{
"type": "Text",
"props": {
"content": { "$state": "/user/name" }
}
}With state { "user": { "name": "Alice" } }, the content prop resolves to "Alice".
Nested paths work with standard JSON Pointer syntax:
{ "$state": "/settings/theme/mode" }
{ "$state": "/items/0/title" }If the path does not exist, the expression resolves to undefined.
$item — Read from Repeat Item
Inside a repeat scope, read a field from the current array item:
{
"type": "Text",
"props": {
"content": { "$item": "title" }
}
}If the repeat source is /todos and the current item is { "title": "Buy milk", "done": false }, this resolves to "Buy milk".
Use an empty string to get the entire item:
{ "$item": "" }Dot-separated paths are supported for nested fields:
{ "$item": "address.city" }$index — Current Repeat Index
Returns the zero-based index of the current item within a repeat scope:
{
"type": "Badge",
"props": {
"label": { "$index": true }
}
}To use the index in a string, combine with $template and reference the index via $cond or concatenation — note that $template interpolates state paths, not the repeat index directly. For display strings like "Item #1", use $computed or build labels from $index:
{
"type": "Text",
"props": {
"content": {
"$computed": "formatIndex",
"args": { "index": { "$index": true } }
}
}
}$bindState — Two-Way State Binding
Creates a two-way binding between a prop and a state path. The prop reads the current value from state, and when the component updates the value (e.g. user types in an input), the state is automatically written back:
{
"type": "Input",
"props": {
"label": "Email",
"value": { "$bindState": "/form/email" }
}
}This is the primary mechanism for form inputs. The component receives both the current value and a callback to update it. No explicit on.change action is needed for basic value synchronization.
$bindState is equivalent to $state for reads. The difference is that the renderer also extracts the path into a bindings map, allowing the component wrapper to set up a write-back channel.
$bindItem — Two-Way Repeat Item Binding
The repeat-scoped equivalent of $bindState. Binds a prop to a field on the current repeat item:
{
"type": "Checkbox",
"props": {
"checked": { "$bindItem": "done" }
}
}Inside a repeat over /todos, for the item at index 2, this resolves to the absolute state path /todos/2/done for writes, and reads the current value of that field.
$cond — Conditional Values
Select between two values based on a condition. The $cond field uses the same visibility condition system:
{
"type": "Badge",
"props": {
"variant": {
"$cond": { "$state": "/user/isAdmin" },
"$then": "default",
"$else": "secondary"
}
}
}The condition is evaluated using the visibility engine. If it passes, $then is returned; otherwise $else (which defaults to undefined if omitted).
Both $then and $else can themselves be expressions, enabling nested conditionals:
{
"$cond": { "$state": "/status", "eq": "error" },
"$then": "destructive",
"$else": {
"$cond": { "$state": "/status", "eq": "success" },
"$then": "default",
"$else": "secondary"
}
}You can use comparison operators in the condition:
{
"$cond": { "$state": "/cart/total", "gt": 100 },
"$then": "Free shipping!",
"$else": { "$template": "Add ${/remaining} more for free shipping" }
}$computed — Custom Functions
Call a named function registered on the Renderer:
{
"type": "Text",
"props": {
"content": {
"$computed": "formatCurrency",
"args": { "amount": { "$state": "/cart/total" }, "currency": "USD" }
}
}
}Register the function via the functions prop:
<Renderer
spec={spec}
registry={prototyperComponents}
functions={{
formatCurrency: ({ amount, currency }) =>
new Intl.NumberFormat("en-US", { style: "currency", currency }).format(amount as number),
uppercase: ({ text }) => String(text).toUpperCase(),
}}
/>Arguments in args are themselves resolved as expressions before being passed to the function. This means you can use $state, $item, or any other expression inside args.
$template — String Interpolation
Interpolate state values into a template string. References use ${/json/pointer/path} syntax:
{
"type": "Text",
"props": {
"content": { "$template": "Hello, ${/user/name}! You have ${/notifications/count} new messages." }
}
}With state { "user": { "name": "Alice" }, "notifications": { "count": 5 } }, this resolves to "Hello, Alice! You have 5 new messages.".
- Paths must start with
/(JSON Pointer format) - Missing values resolve to an empty string
- Non-string values are coerced via
String()
When to Use Each Expression
| Scenario | Expression | Example |
|---|---|---|
| Display a state value | $state | Show user's name in a heading |
| Bind a form input | $bindState | Input value synced to state |
| Display repeat item data | $item | Show each todo's title in a list |
| Edit repeat item data | $bindItem | Checkbox for each todo's "done" field |
| Show item number | $index | "Item #1", "Item #2", etc. |
| Toggle variant/style | $cond | Red badge for errors, green for success |
| Format/compute values | $computed | Currency formatting, date formatting |
| Build dynamic strings | $template | "Welcome back, Alice!" |
Resolution Order
When the renderer encounters a prop value, it checks for expressions in this order:
$state— read from global state$item— read from repeat item$index— return repeat index$bindState— read from global state (also extracts binding path)$bindItem— read from repeat item (also extracts binding path)$cond— evaluate condition and resolve$thenor$else$computed— call registered function$template— interpolate string
Plain objects that do not match any expression pattern are recursively resolved (each value is checked for expressions). Arrays are resolved element-by-element. Primitives (string, number, boolean, null) pass through unchanged.
Next Steps
- Visibility — Conditional rendering using the same condition system as
$cond - Actions — Using expressions in action parameters
- Spec Format — The full spec structure including repeat bindings