Skip to main content

UI Patterns & render-ui

Source: tests/schemas/08-patterns.orb

Orb UI is driven entirely by render-ui effects inside state machine transitions. There is no JSX, no template files, no separate component tree — the state machine is the UI logic.

Page Layoutheadersidebarmain

How render-ui Works

(render-ui slot { type: "pattern", ...props })
ArgumentDescription
"slot"Where on the page the component renders
{ "type": "..." }Which pattern component to use
...propsPattern-specific configuration

To clear a slot:

(render-ui slot null)

Slots

Slots divide the page into named regions. Each slot is owned by one trait at a time.

SlotTypical use
mainPrimary content area
modalModal dialogs (forms, confirmations)
drawerSide panel (detail view)
sidebarPersistent side navigation
overlayFull-screen overlays
hud-top / hud-bottomPersistent headers/footers
toastNotification toasts

Pattern Categories

Display Patterns

entity-table — Data table with columns, sorting, and row actions.

(render-ui main {
type: "entity-table",
entity: "Product",
columns: ["name", "price", "stock", "category"],
itemActions: [
{ event: "VIEW", label: "View" },
{ event: "EDIT", label: "Edit" },
{ event: "DELETE", label: "Delete" }
]
})

entity-detail — Read-only detail view for a single record.

(render-ui main {
type: "entity-detail",
entity: "Product",
fields: ["name", "description", "price", "stock", "category"]
})

stats — Dashboard stat cards (counts, totals, summaries).

(render-ui main {
type: "stats",
items: [
{ label: "Total Products", value: "@entity.count" },
{ label: "Out of Stock", value: "@entity.outOfStock" }
]
})

Form Patterns

form — Auto-generated form for an entity. Renders all fields or a specified subset.

(render-ui main {
type: "form",
entity: "Product",
fields: [
{ name: "name", label: "Product Name", required: true },
{ name: "description", label: "Description", type: "textarea" },
{ name: "price", label: "Price", type: "number", required: true },
{ name: "stock", label: "Stock", type: "number" },
{ name: "category", label: "Category" }
]
})

form-section — A form inside a modal or drawer, with submit/cancel wired to events.

(render-ui modal {
type: "form-section",
entity: "Task",
fields: ["title", "priority", "dueDate"],
submitEvent: "SAVE",
cancelEvent: "CANCEL"
})

Important: Use submitEvent and cancelEvent (not onSubmit/onCancel — those are deprecated).


page-header — Page title with optional action buttons.

(render-ui main {
type: "page-header",
title: "Products",
subtitle: "Manage your product catalog",
actions: [
{ event: "CREATE", label: "New Product", variant: "primary" }
]
})

breadcrumb — Navigation trail.

(render-ui main {
type: "breadcrumb",
items: [
{ label: "Products", path: "/products" },
{ label: "@entity.name" }
]
})

State Patterns

empty-state — Shown when a list has no items.

(render-ui main {
type: "empty-state",
title: "No products yet",
description: "Add your first product to get started",
actions: [{ event: "CREATE", label: "Add Product" }]
})

loading-state — Spinner while data loads.

(render-ui main {
type: "loading-state",
title: "Loading products..."
})

State-Driven UI: Full Example

The power of render-ui is that it changes based on state. Different states render different components into the same slot. Here's the full ProductCRUD trait from 08-patterns.orb:

trait ProductCRUD -> Product [interaction] {
initial: listing
state listing {
INIT -> listing
(fetch Product)
(render-ui main {
type: "entity-table",
entity: "Product",
columns: ["name", "price", "stock", "category"],
itemActions: [
{ event: "VIEW", label: "View" },
{ event: "EDIT", label: "Edit" },
{ event: "DELETE", label: "Delete" }
]
})
VIEW -> viewing
(fetch Product @payload.id)
(render-ui main {
type: "entity-detail",
entity: "Product",
fields: ["name", "description", "price", "stock", "category"]
})
CREATE -> creating
(render-ui main {
type: "form",
entity: "Product",
fields: [
{ name: "name", label: "Product Name", required: true },
{ name: "description", label: "Description", type: "textarea" },
{ name: "price", label: "Price", type: "number", required: true },
{ name: "stock", label: "Stock", type: "number" },
{ name: "category", label: "Category" }
]
})
DELETE -> listing
(persist delete Product @payload.id)
(notify info "Product deleted")
}
state viewing {
EDIT -> editing
(render-ui main { type: "form", entity: "Product", mode: "edit" })
BACK -> listing
(navigate "/products")
}
state editing {
SAVE -> viewing
(persist update Product @entity)
(notify success "Product saved successfully")
CANCEL -> viewing
}
state creating {
SAVE -> listing
(persist update Product @entity)
(notify success "Product created successfully")
(navigate "/products")
CANCEL -> listing
(navigate "/products")
}
}

With pages:

page "/products" -> ProductCRUD
page "/products/:id" -> ProductCRUD

What the state machine renders per state:

Statemain slot renders
listingentity-table with row actions
viewingentity-detail with fields
editingform in edit mode
creatingform with all fields

Action Props Reference

Actions are defined inside the pattern props, not as separate patterns.

PatternHow to wire actions
entity-tableitemActions: [{ "event": "EDIT", "label": "Edit" }]
entity-detailactions: [{ "event": "EDIT", "label": "Edit" }]
form-sectionsubmitEvent: "SAVE", cancelEvent: "CANCEL"
page-headeractions: [{ "event": "CREATE", "label": "New" }]
empty-stateactions: [{ "event": "CREATE", "label": "Add" }]

Bindings in Pattern Props

Pattern props accept bindings to read live data:

BindingResolves to
@entity.fieldCurrent entity field value
@payload.fieldEvent payload field
@stateCurrent state name
@nowCurrent timestamp

Example:

(render-ui main { type: "stats", title: "Cart Total: [email protected]" })

Next Steps