Conditional Rendering
Show and hide elements based on state, repeat context, and compound logic
The visible field on any element controls whether it renders. When visible evaluates to false, the element and all its children are removed from the tree entirely (not just hidden with CSS).
Basic Usage
The simplest condition checks if a state value is truthy:
{
"type": "Alert",
"props": { "title": "Welcome back!" },
"visible": { "$state": "/user/isLoggedIn" }
}This element renders only when /user/isLoggedIn is truthy (not null, undefined, false, 0, or "").
State Conditions
A state condition reads a value from the global state model:
type StateCondition = {
$state: string // JSON Pointer path
eq?: unknown // Strict equality
neq?: unknown // Strict inequality
gt?: number // Greater than
gte?: number // Greater than or equal
lt?: number // Less than
lte?: number // Less than or equal
not?: boolean // Invert the result
}Truthy Check (Default)
When no comparison operator is specified, the condition checks truthiness:
{ "$state": "/user/name" }Renders when /user/name is a non-empty value.
Equality
{ "$state": "/currentTab", "eq": "settings" }Renders only when /currentTab is exactly "settings".
Inequality
{ "$state": "/status", "neq": "loading" }Renders when /status is anything other than "loading".
Numeric Comparisons
{ "$state": "/cart/itemCount", "gt": 0 }
{ "$state": "/user/age", "gte": 18 }
{ "$state": "/inventory/stock", "lt": 10 }
{ "$state": "/form/progress", "lte": 100 }Negation with not
The not flag inverts the final result of any condition:
{ "$state": "/user/isLoggedIn", "not": true }This renders when the user is NOT logged in. not is applied after the comparison operator, so you can combine them:
{ "$state": "/status", "eq": "error", "not": true }Renders when /status is anything other than "error" (equivalent to neq).
Item Conditions (Repeat Context)
Inside a repeat scope, conditions can reference the current item:
type ItemCondition = {
$item: string // Field name on the repeat item
eq?: unknown
neq?: unknown
gt?: number
gte?: number
lt?: number
lte?: number
not?: boolean
}{
"type": "Badge",
"props": { "variant": "default", "label": "Complete" },
"visible": { "$item": "done", "eq": true }
}This badge renders only for items where done is true.
Index Conditions (Repeat Context)
Conditions on the current repeat index:
{
"type": "Separator",
"props": {},
"visible": { "$index": true, "gt": 0 }
}This separator renders for every item except the first (index > 0), creating dividers between items.
Boolean Literals
For explicit always-on or always-off visibility:
{ "visible": true }
{ "visible": false }true is the default when visible is omitted. Setting visible: false permanently hides an element.
Logical Operators
Implicit AND (Array)
Pass an array of conditions. All must be true:
{
"visible": [
{ "$state": "/user/isLoggedIn" },
{ "$state": "/user/role", "eq": "admin" }
]
}Renders only when the user is logged in AND is an admin.
Explicit AND
The $and operator is equivalent to an array but more explicit:
{
"visible": {
"$and": [
{ "$state": "/user/isLoggedIn" },
{ "$state": "/user/role", "eq": "admin" },
{ "$state": "/feature/adminPanel" }
]
}
}Explicit OR
The $or operator requires at least one condition to be true:
{
"visible": {
"$or": [
{ "$state": "/user/role", "eq": "admin" },
{ "$state": "/user/role", "eq": "moderator" }
]
}
}Renders for admins OR moderators.
Combining AND and OR
Logical operators can be nested for complex conditions:
{
"visible": {
"$and": [
{ "$state": "/user/isLoggedIn" },
{
"$or": [
{ "$state": "/user/role", "eq": "admin" },
{ "$state": "/user/permissions/canEdit" }
]
}
]
}
}Renders when the user is logged in AND (is an admin OR has edit permissions).
Examples
Tab Panel Switching
Show content based on the active tab:
{
"root": "container",
"elements": {
"container": {
"type": "Card",
"props": {},
"children": ["tabs", "profile-panel", "settings-panel", "billing-panel"]
},
"tabs": {
"type": "Tabs",
"props": {
"value": { "$bindState": "/activeTab" },
"items": [
{ "value": "profile", "label": "Profile" },
{ "value": "settings", "label": "Settings" },
{ "value": "billing", "label": "Billing" }
]
}
},
"profile-panel": {
"type": "Text",
"props": { "content": "Profile content here" },
"visible": { "$state": "/activeTab", "eq": "profile" }
},
"settings-panel": {
"type": "Text",
"props": { "content": "Settings content here" },
"visible": { "$state": "/activeTab", "eq": "settings" }
},
"billing-panel": {
"type": "Text",
"props": { "content": "Billing content here" },
"visible": { "$state": "/activeTab", "eq": "billing" }
}
},
"state": { "activeTab": "profile" }
}Form State Toggle
Show a success message after submission, hide the form:
{
"type": "Card",
"props": {},
"children": ["email-input", "submit-btn"],
"visible": { "$state": "/submitted", "not": true }
}{
"type": "Alert",
"props": { "title": "Thanks for subscribing!" },
"visible": { "$state": "/submitted" }
}Authentication Gate
Show different content for logged-in vs. anonymous users:
{
"type": "Button",
"props": { "label": "Sign In" },
"visible": { "$state": "/user/isLoggedIn", "not": true }
}{
"type": "Text",
"props": { "content": { "$template": "Welcome, ${/user/name}!" } },
"visible": { "$state": "/user/isLoggedIn" }
}Repeat Context Filtering
Show a "completed" badge only for done items, and a delete button only for items the user owns:
{
"type": "Badge",
"props": { "label": "Done", "variant": "default" },
"visible": { "$item": "done" }
}{
"type": "Button",
"props": { "label": "Delete", "variant": "destructive", "size": "sm" },
"visible": {
"$and": [
{ "$item": "ownerId", "eq": { "$state": "/currentUserId" } },
{ "$item": "done", "not": true }
]
}
}Dynamic State Comparisons
Comparison operators (gt, gte, lt, lte) accept a ComparisonValue, which can be either a literal number or a { $state: "/path" } reference. This enables comparisons between two state values at runtime.
type ComparisonValue = number | { $state: string }For example, show a product only when its price is within the user's budget:
{
"type": "Card",
"props": { "title": "Premium Plan" },
"visible": { "$state": "/price", "lte": { "$state": "/budget" } }
}At evaluation time, both sides resolve to numbers from state. If /price is 49 and /budget is 100, the condition evaluates as 49 <= 100 (true).
Dynamic comparisons work with all numeric operators:
{ "$state": "/score", "gt": { "$state": "/threshold" } }
{ "$state": "/quantity", "gte": { "$state": "/minOrder" } }
{ "$state": "/temperature", "lt": { "$state": "/maxTemp" } }
{ "$state": "/balance", "lte": { "$state": "/creditLimit" } }Example: Product Filtering by Budget
A product list where items are visible only when their price falls within the user's budget:
{
"root": "container",
"elements": {
"container": {
"type": "Card",
"props": {},
"children": ["budgetSlider", "productList"]
},
"budgetSlider": {
"type": "Slider",
"props": {
"label": "Max Budget",
"min": 0,
"max": 500,
"value": { "$bindState": "/budget" }
}
},
"productList": {
"type": "Card",
"props": {},
"children": ["product-a", "product-b", "product-c"]
},
"product-a": {
"type": "Text",
"props": { "content": "Basic Plan — $29" },
"visible": { "$state": "/prices/basic", "lte": { "$state": "/budget" } }
},
"product-b": {
"type": "Text",
"props": { "content": "Pro Plan — $99" },
"visible": { "$state": "/prices/pro", "lte": { "$state": "/budget" } }
},
"product-c": {
"type": "Text",
"props": { "content": "Enterprise Plan — $299" },
"visible": { "$state": "/prices/enterprise", "lte": { "$state": "/budget" } }
}
},
"state": {
"budget": 200,
"prices": { "basic": 29, "pro": 99, "enterprise": 299 }
}
}As the user moves the budget slider, products dynamically show or hide based on whether their price is within budget.
Item & Index Helpers
When building specs in TypeScript, the visibility helper includes item and index sub-objects for constructing repeat-context conditions:
import { visibility } from "@prototyperai/stream-ui/core"
// Item conditions — reference a field on the current repeat item
visibility.item.when("isActive") // truthy check
visibility.item.unless("isDeleted") // falsy check (not: true)
visibility.item.eq("status", "active") // equality
visibility.item.neq("status", "draft") // inequality
visibility.item.gt("priority", 5) // greater than
visibility.item.gte("score", 80) // greater than or equal
visibility.item.lt("price", { $state: "/maxPrice" }) // less than (dynamic)
visibility.item.lte("quantity", 0) // less than or equal
// Index conditions — reference the current repeat index
visibility.index.eq(0) // first item only
visibility.index.neq(0) // skip first item
visibility.index.gt(0) // all except first
visibility.index.lt(5) // first 5 items
visibility.index.gte(2) // from third item onward
visibility.index.lte(9) // first 10 itemsThese helpers return properly typed ItemCondition and IndexCondition objects. Combine them with visibility.and() and visibility.or() for compound conditions:
// Show a separator between items (not before the first)
const separatorVisible = visibility.index.gt(0)
// Show delete button only for owned, incomplete items
const deleteVisible = visibility.and(
visibility.item.eq("ownerId", currentUserId),
visibility.item.unless("done"),
)Visibility vs. $cond
Both visible and $cond use the same condition system, but they serve different purposes:
| Feature | visible | $cond |
|---|---|---|
| Scope | Controls whether an element renders | Controls the value of a single prop |
| Result | Element in tree or not | $then value or $else value |
| Use when | Showing/hiding entire sections | Switching a variant, label, or style |
Use visible to remove elements from the tree. Use $cond in props to change appearance without removing elements:
{
"type": "Badge",
"props": {
"variant": {
"$cond": { "$state": "/status", "eq": "active" },
"$then": "default",
"$else": "secondary"
},
"label": { "$state": "/status" }
}
}Programmatic Visibility Helpers
When building specs in TypeScript, use the visibility helper for a cleaner API:
import { visibility } from "@prototyperai/stream-ui/core"
const element = {
type: "Alert",
props: { title: "Admin Panel" },
visible: visibility.and(
visibility.when("/user/isLoggedIn"),
visibility.eq("/user/role", "admin"),
),
}
// Other helpers:
visibility.always // true
visibility.never // false
visibility.when("/path") // truthy check
visibility.unless("/path") // falsy check (not: true)
visibility.eq("/path", v) // equality
visibility.neq("/path", v) // inequality
visibility.gt("/path", n) // greater than
visibility.gte("/path", n) // greater than or equal
visibility.lt("/path", n) // less than
visibility.lte("/path", n) // less than or equal
visibility.and(...) // all must be true
visibility.or(...) // at least one must be trueNext Steps
- Expressions —
$condfor conditional prop values - Actions — Trigger state changes that affect visibility
- Spec Format — Full spec structure reference