Components

Every component in a JSON-UI spec is referenced by its "type" string in a flat element map. For the full spec format and workflow, see Getting Started.

Each element follows this shape:

"element_id": {
  "type": "ComponentTypeName",
  "props": {
    "prop_name": "prop_value"
  },
  "children": ["child_id"],
  "action": { "handler": "route.name", "method": "POST" },
  "visible": { "path": "/data/status", "operator": "eq", "value": "active" }
}

The sections below document every built-in component: its props table (with JSON types) and a complete element example.


Component Overview

CategoryComponents
LayoutCard, Grid, Tabs, Separator, Modal, Skeleton, Collapsible, FormSection
Data DisplayText, DataTable, Table, DescriptionList, Badge, Avatar, Progress, Breadcrumb, Pagination, StatCard, Image, CalendarCell
FormsForm, Input, Select, Checkbox, CheckboxList, CheckboxGroup, Switch, Button, ButtonGroup, DropdownMenu
FeedbackAlert, Toast, EmptyState
NavigationSidebar, Header, PageHeader, NotificationDropdown
ActionActionCard
OnboardingChecklist
CommerceProductTile
KanbanKanbanBoard, KanbanColumn
ExtensibleRawHtml, Plugin (see Plugins)

Shared Enum Values

Several props accept fixed-string enum values. The valid strings are listed here; each component section references these by name.

size"xs" | "sm" | "default" | "lg"

button_variant"default" | "secondary" | "destructive" | "outline" | "ghost" | "link"

alert_variant"info" | "success" | "warning" | "error"

badge_variant"default" | "secondary" | "destructive" | "outline"

column_format"date" | "date_time" | "currency" | "boolean"

text_element"p" | "h1" | "h2" | "h3" | "span" | "div" | "section"

toast_variant"info" | "success" | "warning" | "error"

input_type"text" | "email" | "password" | "number" | "textarea" | "hidden" | "date" | "time" | "url" | "tel" | "search"

orientation"horizontal" | "vertical"

icon_position"left" | "right"

sort_direction"asc" | "desc"

form_max_width"default" | "narrow" | "wide"

gap_size"none" | "sm" | "md" (default) | "lg" | "xl"

action_card_variant"default" | "setup" | "danger"


Layout Components

Card

Container with title, optional description, nested children, and footer.

PropTypeDescription
titlestringCard heading
descriptionstring | nullSecondary text below the title
subtitlestring | nullMuted secondary identifier rendered between title and description (e.g. staff name beneath customer name)
badgestring | nullSmall Badge-styled pill rendered to the right of the title (Secondary variant chrome) for status indicators, counters, or countdown labels

Children are element IDs listed in the "children" array on the element, not in props.

"user_card": {
  "type": "Card",
  "props": {
    "title": "User Details",
    "description": "Account information"
  },
  "children": ["name_text", "email_text"]
}

Optional subtitle and badge slots add a muted secondary identifier and a Badge-styled pill respectively. Vertical stacking: title → subtitle → description.

"booking_card": {
  "type": "Card",
  "props": {
    "title": "Booking #1",
    "subtitle": "Marco Rossi",
    "description": "Pending email confirmation",
    "badge": "Scade tra 9m"
  },
  "children": []
}

Children semantics: On container components (Card, Form, Grid, etc.) children is an array of element ID strings that reference entries in the spec's flat elements map. The elements themselves are siblings at the top level — not nested.

{
  "$schema": "ferro-json-ui/v2",
  "root": "card_main",
  "elements": {
    "card_main": {
      "type": "Card",
      "props": { "title": "Welcome" },
      "children": ["heading", "form_login"]
    },
    "heading": {
      "type": "Text",
      "props": { "content": "Sign in", "element": "h2" }
    },
    "form_login": {
      "type": "Form",
      "props": { "max_width": "sm" },
      "children": ["email_input", "submit_btn"],
      "action": { "handler": "auth.login", "method": "POST" }
    },
    "email_input": {
      "type": "Input",
      "props": { "field": "email", "label": "Email", "input_type": "email" }
    },
    "submit_btn": {
      "type": "Button",
      "props": { "label": "Sign in", "button_type": "submit" }
    }
  }
}

All elements — card_main, heading, form_login, email_input, submit_btn — are siblings in the elements map. The tree structure is expressed purely through children ID references.

Variant

Card accepts an optional variant prop controlling chrome and padding.

card_variant"bordered" (default) | "elevated"

ValueClasses appliedPaddingTypical use
"bordered"border border-border bg-card shadow-sm overflow-visiblep-4Dashboard cards in dense layouts
"elevated"bg-card shadow-md overflow-visible (no border)p-8Auth pages, error pages, standalone marketing cards

variant defaults to "bordered" when omitted.

"auth_card": {
  "type": "Card",
  "props": {
    "title": "Sign in",
    "variant": "elevated"
  },
  "children": ["login_form"]
}

Grid

Responsive grid layout for arranging child elements in columns.

PropTypeDescription
columnsnumber | nullNumber of columns (default: 2)
gapgap_size | nullGap between items: "none", "xs", "sm", "md", "lg", "xl"
"stats_grid": {
  "type": "Grid",
  "props": {
    "columns": 3,
    "gap": "md"
  },
  "children": ["revenue_stat", "orders_stat", "users_stat"]
}

Visibility

visible is an element-level field that lives on every JSON-UI element. It is not a GridProps prop. When the visibility condition evaluates false against the spec's data payload, the Grid and all of its children are absent from the rendered DOM — the entire subtree is omitted (no hidden attribute, no empty wrapper).

"staff_chips_row": {
  "type": "Grid",
  "props": { "columns": 1, "gap": "sm" },
  "children": ["staff_chip"],
  "visible": { "path": "/has_staff", "operator": "eq", "value": true }
}

Identical semantics apply to every other v2 component — Card, Form, Button, Badge, and all plugin components. The visibility check runs once per element in the walker before component dispatch (ferro-json-ui/src/render/mod.rs element-level visibility check), so there is no per-component scope shifting and no component-specific visibility behavior.

Tabs

Tabbed content with multiple panels.

PropTypeDescription
default_tabstringValue of the initially active tab
tabsarrayTab definitions

Each object in tabs:

FieldTypeDescription
valuestringTab identifier (matches default_tab)
labelstringTab label text
childrenarray of stringsElement IDs shown when the tab is active
"settings_tabs": {
  "type": "Tabs",
  "props": {
    "default_tab": "general",
    "tabs": [
      { "value": "general", "label": "General", "children": ["general_form"] },
      { "value": "security", "label": "Security", "children": ["security_form"] }
    ]
  }
}

Separator

Visual divider between content sections.

PropTypeDescription
orientationorientation | null"horizontal" (default) or "vertical"
"divider": {
  "type": "Separator",
  "props": {}
}

Dialog overlay with title, body children, footer children, and a trigger button label.

PropTypeDescription
titlestringModal heading
descriptionstring | nullModal description text
trigger_labelstring | nullLabel for the button that opens the modal

Children of the modal body go in the element "children" array. Footer children use a "footer_children" prop listing element IDs.

"delete_modal": {
  "type": "Modal",
  "props": {
    "title": "Delete Item",
    "description": "This action cannot be undone.",
    "trigger_label": "Delete"
  },
  "children": ["confirm_text"],
  "action": { "handler": "items.destroy", "method": "DELETE" }
}

Skeleton

Loading placeholder with configurable dimensions.

PropTypeDescription
widthstring | nullCSS width (e.g., "100%", "200px")
heightstring | nullCSS height (e.g., "40px")
roundedboolean | nullUse rounded corners
"loading_placeholder": {
  "type": "Skeleton",
  "props": {
    "width": "100%",
    "height": "40px",
    "rounded": true
  }
}

Collapsible

An expandable/collapsible section with a trigger label.

PropTypeDescription
triggerstringLabel for the toggle
openboolean | nullInitially open when true
"advanced_section": {
  "type": "Collapsible",
  "props": {
    "trigger": "Advanced Options",
    "open": false
  },
  "children": ["timeout_input", "retry_input"]
}

FormSection

Groups form fields under a section heading with an optional description.

PropTypeDescription
titlestringSection heading
descriptionstring | nullSection description
"billing_section": {
  "type": "FormSection",
  "props": {
    "title": "Billing Information",
    "description": "Used for invoice generation."
  },
  "children": ["address_input", "city_input", "postal_input"]
}

Data Display Components

Text

Renders text content with a semantic HTML element.

PropTypeDescription
contentstringText content
elementtext_element | nullHTML element: "p" (default), "h1", "h2", "h3", "span", "div", "section"
"page_heading": {
  "type": "Text",
  "props": {
    "content": "Welcome to the dashboard",
    "element": "h1"
  }
}

Content can use a $template expression to interpolate data:

"greeting": {
  "type": "Text",
  "props": {
    "content": { "$template": "Welcome, {/user/name}!" },
    "element": "h2"
  }
}

DataTable

Data-bound table with column definitions, row actions, and sorting. Rows are loaded from the spec's data via data_path.

PropTypeDescription
columnsarrayColumn definitions (see below)
data_pathstringJSON Pointer to the row data array (e.g., "/orders")
row_actionsarray | nullActions available per row
empty_messagestring | nullMessage when no data is present
sortableboolean | nullEnable column sorting
sort_columnstring | nullCurrently sorted column key
sort_directionsort_direction | null"asc" or "desc"

Each column object:

FieldTypeDescription
keystringData field key in the row object
labelstringColumn header text
formatcolumn_format | nullDisplay format
"users_table": {
  "type": "DataTable",
  "props": {
    "data_path": "/users",
    "columns": [
      { "key": "name", "label": "Name" },
      { "key": "email", "label": "Email" },
      { "key": "created_at", "label": "Created", "format": "date" }
    ],
    "row_actions": [
      { "handler": "users.edit", "method": "GET" },
      { "handler": "users.destroy", "method": "DELETE", "confirm": { "message": "Delete this user?" } }
    ],
    "empty_message": "No users found.",
    "sortable": true
  }
}

Table

Simple table without a data binding path. Use DataTable for data-bound tables; use Table for static content.

PropTypeDescription
columnsarrayColumn definitions (same structure as DataTable)
rowsarrayStatic row objects (key-value maps)
"static_table": {
  "type": "Table",
  "props": {
    "columns": [
      { "key": "plan", "label": "Plan" },
      { "key": "price", "label": "Price", "format": "currency" }
    ],
    "rows": [
      { "plan": "Starter", "price": "9.00" },
      { "plan": "Pro", "price": "29.00" }
    ]
  }
}

DescriptionList

Key-value pairs displayed as a description list.

PropTypeDescription
itemsarrayDescription items (see below)
columnsnumber | nullNumber of columns for layout

Each item object:

FieldTypeDescription
labelstringItem label
valuestringItem value
formatcolumn_format | nullDisplay format
"user_info": {
  "type": "DescriptionList",
  "props": {
    "columns": 2,
    "items": [
      { "label": "Name", "value": { "$data": "/user/name" } },
      { "label": "Joined", "value": { "$data": "/user/created_at" }, "format": "date" },
      { "label": "Active", "value": { "$data": "/user/active" }, "format": "boolean" }
    ]
  }
}

Dynamic items via data_path

data_path (optional) takes precedence over items when set. It resolves to a JSON array decoded as Vec<DescriptionItem> ({ "label": string, "value": string, "format"?: string }). Falls back to items when the path is missing.

"document_details": {
  "type": "DescriptionList",
  "props": {
    "columns": 2,
    "data_path": "/document/fields"
  }
}

Handler data: { "document": { "fields": [{ "label": "Author", "value": "Alice" }, { "label": "Created", "value": "2026-05-17", "format": "date" }] } }.

Badge

Small label with variant-based styling.

PropTypeDescription
labelstringBadge text
variantbadge_variant | nullVisual style (default: "default")
"status_badge": {
  "type": "Badge",
  "props": {
    "label": "Active",
    "variant": "default"
  }
}

Avatar

User avatar with image, fallback initials, and size.

PropTypeDescription
altstringAlt text (required for accessibility)
srcstring | nullImage URL
fallbackstring | nullFallback initials when no image
sizesize | null"xs", "sm", "default", "lg"
"user_avatar": {
  "type": "Avatar",
  "props": {
    "alt": "Alice Johnson",
    "src": { "$data": "/user/avatar_url" },
    "fallback": "AJ",
    "size": "lg"
  }
}

Progress

Progress bar with a percentage value.

PropTypeDescription
valuenumberPercentage value (0-100)
maxnumber | nullMaximum value
labelstring | nullLabel text above the bar
"upload_progress": {
  "type": "Progress",
  "props": {
    "value": 75,
    "max": 100,
    "label": "Uploading..."
  }
}

Navigation breadcrumb trail.

PropTypeDescription
itemsarrayBreadcrumb items (see below)

Each item object:

FieldTypeDescription
labelstringBreadcrumb text
urlstring | nullLink URL (omit for the current page)
"breadcrumbs": {
  "type": "Breadcrumb",
  "props": {
    "items": [
      { "label": "Home", "url": "/" },
      { "label": "Users", "url": "/users" },
      { "label": "Edit User" }
    ]
  }
}

Pagination

Page navigation for paginated data.

PropTypeDescription
current_pagenumberCurrent page number
per_pagenumberItems per page
totalnumberTotal item count
base_urlstring | nullBase URL for page links
"users_pagination": {
  "type": "Pagination",
  "props": {
    "current_page": { "$data": "/meta/page" },
    "per_page": 25,
    "total": { "$data": "/meta/total" },
    "base_url": "/users"
  }
}

StatCard

Metric card for dashboards. Displays a label and value, with an optional SSE target for live updates.

PropTypeDescription
labelstringMetric label (e.g., "Total Revenue")
valuestringCurrent metric value (e.g., "€12,345")
iconstring | nullIcon name
subtitlestring | nullSecondary text below the value
sse_targetstring | nullSSE event key for live value updates
"revenue_stat": {
  "type": "StatCard",
  "props": {
    "label": "Total Revenue",
    "value": { "$data": "/stats/revenue_formatted" },
    "icon": "currency-euro",
    "subtitle": "This month",
    "sse_target": "revenue_total"
  }
}

When sse_target is set and the server emits a Server-Sent Event with a matching key, the runtime updates the displayed value in place:

event: live-value
data: {"target": "revenue_total", "value": "€13,210"}

Image

Renders an <img> element.

PropTypeDescription
srcstringImage URL
altstringAlt text
widthnumber | nullCSS width in pixels
heightnumber | nullCSS height in pixels
classstring | nullAdditional CSS classes
"hero_image": {
  "type": "Image",
  "props": {
    "src": "/images/hero.jpg",
    "alt": "Dashboard hero",
    "width": 1200,
    "height": 400
  }
}

Dynamic source via data_path

data_path (optional) takes precedence over src when set. It is a JSON Pointer resolved against handler data at render time; the resolved value is used as the <img src> attribute. Falls back to the static src value when the path is missing or resolves to a non-string.

"product_image": {
  "type": "Image",
  "props": {
    "src": "/images/placeholder.jpg",
    "alt": "Product image",
    "data_path": "/product/image_url"
  }
}

Handler data: { "product": { "image_url": "/uploads/product-42.jpg" } } → rendered src is /uploads/product-42.jpg.

CalendarCell

Renders a single day cell in a month grid. Intended for use inside a custom calendar layout; not a standalone page component.

PropTypeDescription
daynumberDay of month (1–31)
is_todayboolean | nullHighlights the cell as today (default: false)
is_current_monthboolean | nullDims the cell when outside the current month (default: false)
event_countnumber | nullEvent indicator dot count (default: 0)
dot_colorsarray | nullPer-event Tailwind color classes (e.g. "bg-blue-500"). When non-empty, colored dots replace plain primary dots.
"day_14": {
  "type": "CalendarCell",
  "props": {
    "day": 14,
    "is_today": true,
    "is_current_month": true,
    "event_count": 3,
    "dot_colors": ["bg-blue-500", "bg-green-500", "bg-red-500"]
  }
}

Form Components

Form

Form container with an action binding. Field components go in the element "children" array.

PropTypeDescription
methodstring | nullHTTP method override ("GET", "POST", "PUT", "PATCH", "DELETE")
max_widthform_max_width | nullMax form width: "sm", "md", "lg", "xl", "full"

The submit action is set on the element's "action" field, not in props.

"create_form": {
  "type": "Form",
  "props": {
    "max_width": "md"
  },
  "children": ["name_input", "email_input", "submit_btn"],
  "action": { "handler": "users.store", "method": "POST" }
}

Input

Text input field with type, label, validation error, and optional data binding.

PropTypeDescription
fieldstringForm field name
labelstringInput label
input_typeinput_type | nullInput type (default: "text")
placeholderstring | nullPlaceholder text
requiredboolean | nullMark as required
disabledboolean | nullDisable the field
errorstring | nullValidation error message
descriptionstring | nullHelp text below the input
default_valuestring | nullPre-filled static value
data_pathstring | nullJSON Pointer for pre-filling from handler data
stepstring | nullHTML step attribute for number inputs (e.g., "0.01")

data_path is a plain string JSON Pointer (not a $data expression). The renderer reads the value from the spec data at that pointer and pre-fills the field.

"email_input": {
  "type": "Input",
  "props": {
    "field": "email",
    "label": "Email Address",
    "input_type": "email",
    "placeholder": "user@example.com",
    "required": true,
    "description": "Your work email",
    "data_path": "/user/email"
  }
}

Select

Dropdown select field with options and optional data binding.

PropTypeDescription
fieldstringForm field name
labelstringSelect label
optionsarrayOption objects: { "value": string, "label": string }
placeholderstring | nullPlaceholder text
requiredboolean | nullMark as required
disabledboolean | nullDisable the field
errorstring | nullValidation error message
descriptionstring | nullHelp text below the select
default_valuestring | nullPre-selected static value
data_pathstring | nullJSON Pointer for pre-selecting from handler data
"role_select": {
  "type": "Select",
  "props": {
    "field": "role",
    "label": "Role",
    "placeholder": "Select a role",
    "required": true,
    "data_path": "/user/role",
    "options": [
      { "value": "admin", "label": "Administrator" },
      { "value": "editor", "label": "Editor" },
      { "value": "viewer", "label": "Viewer" }
    ]
  }
}

Checkbox

Boolean checkbox field.

PropTypeDescription
fieldstringForm field name
labelstringCheckbox label
descriptionstring | nullHelp text below the checkbox
checkedboolean | nullDefault checked state
data_pathstring | nullJSON Pointer for pre-filling from handler data
requiredboolean | nullMark as required
disabledboolean | nullDisable the field
errorstring | nullValidation error message
"terms_checkbox": {
  "type": "Checkbox",
  "props": {
    "field": "terms",
    "label": "Accept Terms of Service",
    "description": "You must accept to continue.",
    "required": true
  }
}

Switch

State-flip toggle. Use Switch when the semantic is "flip this state" (on/off, open/closed, enabled/disabled) — distinct from Checkbox, which expresses a binary choice within a set of options. The renderer emits role="switch" and aria-checked so browsers and assistive technology recognize the toggle affordance.

PropTypeDescription
fieldstringForm field name
labelstringSwitch label
descriptionstring | nullHelp text below the switch
checkedboolean | nullDefault checked state
data_pathstring | nullJSON Pointer for pre-filling from handler data
requiredboolean | nullMark as required
disabledboolean | nullDisable the field
compactboolean | nullScale the toggle down (scale-75) for use in dense grid layouts
errorstring | nullValidation error message
actionAction | nullWhen present, wraps the switch in a <form> and auto-submits on change
"day_open_switch": {
  "type": "Switch",
  "props": {
    "field": "day_1_is_open",
    "label": "Aperto",
    "data_path": "/schedule/day_1_is_open",
    "compact": true,
    "action": { "handler": "schedule.toggle_day", "method": "POST", "url": "/schedule/toggle" }
  }
}

Substitution: Checkbox styled as switch

For consumers who do not need state-flip semantics and prefer to compose from Checkbox primitives, render a Checkbox and apply the Tailwind utility classes that yield a switch appearance (rounded-full track, translated indicator, etc.). Switch remains the recommended path when the semantic is "flip this state" — its dedicated rendering emits the role="switch" ARIA marker and the visual affordance browsers and assistive technology recognize.

There is no variant: "switch" prop on Checkbox today. The substitution is purely visual via custom class hooks, not an API-level feature.

CheckboxList

A group of checkboxes sharing a single form field name. Each checked option submits as field=value. Supports both static option lists and data-driven options resolved from handler data.

PropTypeDescription
fieldstringForm field name; each selected checkbox submits as field=value
optionsarray | nullStatic option list: [{ "value": string, "label": string }]
options_pathstring | nullJSON Pointer to a data array of { "value", "label" } objects (used when options is empty)
selected_pathstring | nullJSON Pointer to a string[] of pre-selected values
labelstring | nullGroup label
descriptionstring | nullHelp text below the group
disabledboolean | nullDisable all checkboxes
errorstring | nullValidation error message

options_path and selected_path are plain JSON Pointer strings, not $data expressions.

"services_list": {
  "type": "CheckboxList",
  "props": {
    "field": "services",
    "label": "Choose Services",
    "options_path": "/available_services",
    "selected_path": "/user/selected_services"
  }
}

CheckboxGroup

An alias for CheckboxList. Accepts identical props and produces identical HTML output — a <fieldset> with one <input type="checkbox"> per option, each with name="field" for form submission. Use whichever name reads more clearly in a given spec; there is no behavioral difference.

"copy_targets": {
  "type": "CheckboxGroup",
  "props": {
    "field": "copy_to",
    "label": "Copia su",
    "options": [
      { "value": "tue", "label": "Martedì" },
      { "value": "wed", "label": "Mercoledì" },
      { "value": "thu", "label": "Giovedì" }
    ]
  }
}

Each checked option submits as copy_to=<value>. When multiple options are checked, the browser sends repeated copy_to parameters (standard HTML multi-value form semantics).

Substitution: composing from Checkbox primitives

As an alternative, the same array-submit semantics can be composed directly from individual Checkbox elements whose field ends in []. The [] suffix causes the browser to collect all checked values under a single array key:

"copy_tue": {
  "type": "Checkbox",
  "props": { "field": "copy_to[]", "label": "Martedì", "value": "tue" }
},
"copy_wed": {
  "type": "Checkbox",
  "props": { "field": "copy_to[]", "label": "Mercoledì", "value": "wed" }
},
"copy_thu": {
  "type": "Checkbox",
  "props": { "field": "copy_to[]", "label": "Giovedì", "value": "thu" }
}

Each checked input submits as copy_to[]=<value>, which most server frameworks decode as an array under the key copy_to.

When to use CheckboxGroup: data-driven multi-select where the option list comes from handler data (options_path) or is defined once statically. Compact and concise.

When to compose from Checkbox: per-option conditional visibility ("visible" rules), per-option custom layout inside a Grid or FormSection, or per-option data_path binding. The explicit form is more verbose but gives full control over each item's placement and visibility.

Button

Interactive button. Attach the click action on the element's "action" field.

PropTypeDescription
labelstringButton label
variantbutton_variant | nullVisual style (default: "default")
sizesize | nullButton size (default: "default")
disabledboolean | nullDisable the button
iconstring | nullIcon name
icon_positionicon_position | null"left" (default) or "right"
button_typestring | nullHTML button type: "button", "submit", "reset"
"save_btn": {
  "type": "Button",
  "props": {
    "label": "Save Changes",
    "variant": "default",
    "size": "default",
    "icon": "save",
    "icon_position": "left"
  },
  "action": { "handler": "profile.update", "method": "PUT" }
}

ButtonGroup

A horizontal group of buttons rendered together.

PropTypeDescription
buttonsarrayButton definitions (same props as Button, plus "action")
"filter_group": {
  "type": "ButtonGroup",
  "props": {
    "buttons": [
      { "label": "All", "variant": "default" },
      { "label": "Active", "variant": "outline" },
      { "label": "Archived", "variant": "outline" }
    ]
  }
}

A button that opens a dropdown with action items. Useful for per-row table actions.

PropTypeDescription
labelstringTrigger button label
actionsarrayAction items (see below)

Each action object:

FieldTypeDescription
labelstringMenu item text
handlerstringRoute handler name
methodstringHTTP method
variantstring | null"destructive" for danger actions
"row_actions": {
  "type": "DropdownMenu",
  "props": {
    "label": "Actions",
    "actions": [
      { "label": "View Details", "handler": "orders.show", "method": "GET" },
      { "label": "Delete", "handler": "orders.destroy", "method": "DELETE", "variant": "destructive" }
    ]
  }
}

Feedback Components

Alert

Alert message with variant-based styling and optional title.

PropTypeDescription
messagestringAlert message content
variantalert_variant | nullVisual style (default: "info")
titlestring | nullAlert title
"trial_warning": {
  "type": "Alert",
  "props": {
    "message": "Your trial expires in 3 days.",
    "variant": "warning",
    "title": "Trial Ending"
  }
}

Toast

Declarative notification rendered as an overlay by the JS runtime. When a Toast element is in the spec, the runtime displays it on page load and dismisses it after the timeout.

PropTypeDescription
messagestringToast message content
varianttoast_variant | nullVisual style (default: "info")
timeoutnumber | nullSeconds before auto-dismiss (default: 5)
dismissibleboolean | nullAllow manual dismiss (default: true)
"save_toast": {
  "type": "Toast",
  "props": {
    "message": "Changes saved successfully.",
    "variant": "success",
    "timeout": 3,
    "dismissible": true
  }
}

EmptyState

Displayed when a list or table has no data. Provides a call-to-action.

PropTypeDescription
titlestringEmpty state heading
descriptionstring | nullSupporting text
action_labelstring | nullCTA button label
iconstring | nullIcon name

Pair with an element "action" for the CTA navigation.

"no_orders": {
  "type": "EmptyState",
  "props": {
    "title": "No orders yet",
    "description": "Create your first order to get started.",
    "action_label": "New Order",
    "icon": "shopping-bag"
  },
  "action": { "handler": "orders.create", "method": "GET" }
}

Sidebar navigation shell with fixed top items, grouped items, and fixed bottom items. Typically used inside the dashboard layout.

PropTypeDescription
fixed_toparray | nullItems pinned at the top (e.g., logo/home)
groupsarray | nullCollapsible navigation groups
fixed_bottomarray | nullItems pinned at the bottom (e.g., settings, logout)

Navigation item object:

FieldTypeDescription
labelstringLink text
hrefstringLink URL
iconstring | nullIcon name
activeboolean | nullMark as current page

Navigation group object:

FieldTypeDescription
labelstringGroup heading
collapsedboolean | nullStart collapsed
itemsarrayNavigation items in this group
"sidebar": {
  "type": "Sidebar",
  "props": {
    "fixed_top": [
      { "label": "Dashboard", "href": "/", "icon": "home", "active": true }
    ],
    "groups": [
      {
        "label": "Management",
        "collapsed": false,
        "items": [
          { "label": "Users", "href": "/users", "icon": "users" },
          { "label": "Orders", "href": "/orders", "icon": "shopping-bag" }
        ]
      }
    ],
    "fixed_bottom": [
      { "label": "Settings", "href": "/settings", "icon": "cog" }
    ]
  }
}

Application header with business name, user info, notification count, and logout link. Typically used inside the dashboard layout.

PropTypeDescription
business_namestringApplication name
notification_countnumber | nullUnread notification count
user_namestring | nullCurrent user's name
user_avatarstring | nullCurrent user's avatar URL
logout_urlstring | nullLogout link URL
"app_header": {
  "type": "Header",
  "props": {
    "business_name": "My App",
    "notification_count": { "$data": "/notifications/unread" },
    "user_name": { "$data": "/auth/user/name" },
    "logout_url": "/logout"
  }
}

Page-level header with a title, optional subtitle, optional breadcrumb, and optional action buttons.

PropTypeDescription
titlestringPage title
breadcrumbarray | nullBreadcrumb items (same shape as Breadcrumb items)
actionsarray | nullElement IDs of action button elements rendered to the right of the title
"page_header": {
  "type": "PageHeader",
  "props": {
    "title": "Orders",
    "breadcrumb": [
      { "label": "Home", "url": "/" },
      { "label": "Orders" }
    ],
    "actions": ["new_order_btn"]
  }
}

actions — lax acceptance

actions accepts any of the following forms, all of which deserialize to an empty or populated list:

Wire valueResult
omittedempty list
nullempty list
"" (empty string)empty list
["btn_id", ...]list of element IDs

Controllers that pass "" or omit the field when there are no actions do not need a special-case branch — all lax forms produce an empty list.

NotificationDropdown

A dropdown list of notification items, typically rendered inside a Header.

PropTypeDescription
notificationsarrayNotification items (see below)
empty_textstring | nullText when list is empty

Each notification object:

FieldTypeDescription
textstringNotification message
iconstring | nullIcon name
timestampstring | nullHuman-readable time string
readboolean | nullWhether the notification has been read
action_urlstring | nullURL to navigate to on click
"notifications": {
  "type": "NotificationDropdown",
  "props": {
    "empty_text": "No new notifications",
    "notifications": [
      {
        "icon": "bell",
        "text": "New order received",
        "timestamp": "5 minutes ago",
        "read": false,
        "action_url": "/orders/123"
      },
      {
        "text": "Payment processed",
        "timestamp": "1 hour ago",
        "read": true
      }
    ]
  }
}

Action Components

ActionCard

A card that acts as a clickable action item.

PropTypeDescription
titlestringCard heading
descriptionstring | nullSupporting text
iconstring | nullIcon name
variantaction_card_variant | nullVisual style: "default", "outline", "ghost"
"create_product": {
  "type": "ActionCard",
  "props": {
    "title": "Add Product",
    "description": "Create a new product listing.",
    "icon": "plus",
    "variant": "outline"
  },
  "action": { "handler": "products.create", "method": "GET" }
}

Onboarding Components

Checklist

Step-by-step onboarding checklist with optional server-side state persistence.

PropTypeDescription
titlestringChecklist heading
itemsarrayChecklist items (see below)
dismissibleboolean | nullAllow dismissal (default: true)
dismiss_labelstring | nullCustom dismiss button label
data_keystring | nullServer-side state persistence key

Each item object:

FieldTypeDescription
labelstringStep description
checkedboolean | nullWhether this step is complete
hrefstring | nullLink to complete the step
"setup_checklist": {
  "type": "Checklist",
  "props": {
    "title": "Get Started",
    "dismissible": true,
    "dismiss_label": "Done",
    "data_key": "onboarding_checklist",
    "items": [
      { "label": "Create your account", "checked": true },
      { "label": "Set up billing", "checked": false, "href": "/billing" },
      { "label": "Invite your team", "checked": false, "href": "/team/invite" }
    ]
  }
}

Commerce Components

ProductTile

Product display card with image, title, price, and optional action.

PropTypeDescription
titlestringProduct name
pricestringFormatted price string (e.g., "€29.00")
descriptionstring | nullProduct description
image_urlstring | nullProduct image URL
badgestring | nullBadge text (e.g., "New", "Sale")
action_labelstring | nullAction button label
"product_tile": {
  "type": "ProductTile",
  "props": {
    "title": { "$data": "/product/name" },
    "price": { "$data": "/product/price_formatted" },
    "description": { "$data": "/product/description" },
    "image_url": { "$data": "/product/image_url" },
    "badge": "New",
    "action_label": "Add to Cart"
  },
  "action": { "handler": "cart.add", "method": "POST" }
}

Kanban Components

KanbanBoard

Kanban board with fixed lanes. On mobile, lanes switch to tabs.

A kanban is fixed lanes plus items sorted into them by a status field. columns is structure (lane id + title) and is always rendered — an empty lane still shows its header and a zero count. Card content is data-bound: items_path resolves a flat array of entity objects, each bucketed into the lane whose id equals the item's group_by value, then rendered as a card via the card_* / row_* bindings. This is the same prescribed-card + field-key convention used by DataTable and MediaCardGrid.

PropTypeDescription
columnsarray | nullLane structure — KanbanColumnProps objects (id + title). Always rendered.
items_pathstring | nullJSON Pointer to a flat array of entity objects to bucket into lanes.
group_bystring | nullField on each item selecting its lane: column.id == item[group_by].
card_title_keystring | nullItem field whose value becomes the card title.
card_description_keystring | nullItem field whose value becomes the card subtitle.
row_actionsarray | nullPer-card dropdown actions. {row_key} / {id} interpolate from the item.
row_keystring | nullItem field used for {row_key} substitution in action URLs (defaults to id).
mobile_default_columnstring | nullLane id selected by default on mobile tab view.
empty_labelstring | nullPlaceholder text shown inside empty lanes.
"order_board": {
  "type": "KanbanBoard",
  "props": {
    "columns": [
      { "id": "pending",    "title": "Pending" },
      { "id": "processing", "title": "Processing" },
      { "id": "done",       "title": "Done" }
    ],
    "items_path": "/data/order",
    "group_by": "status",
    "card_title_key": "name",
    "card_description_key": "total"
  }
}

Handler data — a flat array; the renderer buckets by status, so handlers need no per-lane grouping:

{
  "data": {
    "order": [
      { "id": 1, "name": "#1", "total": "€ 16,00", "status": "pending" },
      { "id": 2, "name": "#2", "total": "€ 40,00", "status": "done" }
    ]
  }
}

For fully-custom card structure (badges, nested elements) rather than the prescribed title/description card, template the cards with the $each directive inside a fixed KanbanColumn instead.

KanbanColumn

A single column in a KanbanBoard.

PropTypeDescription
titlestringColumn heading
data_pathstringJSON Pointer to the card data array
countnumber | nullBadge count shown in the column header
empty_messagestring | nullMessage when the column has no cards
"pending_col": {
  "type": "KanbanColumn",
  "props": {
    "title": "Pending",
    "data_path": "/orders/pending",
    "count": { "$data": "/orders/pending_count" },
    "empty_message": "No pending orders"
  },
  "children": ["pending_card_template"]
}

Extensible Components

RawHtml

Server-injected HTML island for narrow HTML-fragment use cases: status pills, badge decorations, link wrappers, and similar one-off markup that does not warrant a first-class plugin.

PropTypeDescription
htmlstringServer-constructed HTML emitted verbatim into the response
"status_pill": {
  "type": "RawHtml",
  "props": {
    "html": "<span class=\"pill pill-green\">Active</span>"
  }
}

Trust boundary. html is emitted verbatim with no sanitization. The consumer is responsible for ensuring the value is safe before embedding it in the spec. For untrusted input (e.g., user-supplied content), run it through a sanitizer such as ammonia in the handler before assigning it to html. This mirrors the discipline required by RichTextEditor.

For richer widgets that are interactive, need asset injection (CSS/JS bundles), or are reused across multiple pages, use the first-class plugin system instead — see plugins.md.

For plugin components (third-party or custom types not in the built-in catalog), see Plugins.


StreamText

Connects to a server-sent-events endpoint and renders token-by-token output as plain text. Tokens are appended as text nodes — no HTML interpretation.

PropTypeDescription
sse_urlstringURL of the SSE endpoint that streams tokens
placeholderstring?Text shown inside the content area before the first token arrives
loading_textstring?Status indicator shown while the stream is open
"response_area": {
  "type": "StreamText",
  "props": {
    "sse_url": "/ai/generate",
    "placeholder": "Response will appear here…",
    "loading_text": "Generating…"
  }
}

Server contract. The SSE endpoint must emit event: done when the stream is complete:

#![allow(unused)]
fn main() {
tx.send(SseEvent::new().event("done").data("")).await.ok();
}

Without event: done, the browser's EventSource auto-reconnects after the connection closes, causing the component to re-fetch the endpoint in a loop.

Security. Tokens are appended as plain text nodes — innerHTML is never called. Streamed content cannot inject HTML or execute scripts regardless of its content.


Inline view/edit pattern

An inline view/edit page is built from a Form element whose children include both read-only display items and editable inputs, each toggled by a visible condition on a query parameter.

This pattern requires no Rust code to distinguish view and edit modes — the spec handles it entirely through visible rules on query.mode.

{
  "$schema": "ferro-json-ui/v2",
  "title": "Profile",
  "layout": "dashboard",
  "root": "profile_card",
  "elements": {
    "profile_card": {
      "type": "Card",
      "props": { "title": "Profile" },
      "children": ["edit_btn", "profile_form"]
    },
    "edit_btn": {
      "type": "Button",
      "props": { "label": "Edit", "variant": "outline" },
      "action": { "url": "?mode=edit" },
      "visible": { "ne": ["query.mode", "edit"] }
    },
    "profile_form": {
      "type": "Form",
      "props": { "max_width": "md" },
      "children": [
        "name_view", "name_edit",
        "email_view", "email_edit",
        "save_btn"
      ],
      "action": { "handler": "profile.update", "method": "POST" }
    },
    "name_view": {
      "type": "DescriptionList",
      "props": {
        "items": [{ "label": "Name", "value": { "$data": "/user/name" } }]
      },
      "visible": { "ne": ["query.mode", "edit"] }
    },
    "name_edit": {
      "type": "Input",
      "props": {
        "field": "name",
        "label": "Name",
        "data_path": "/user/name"
      },
      "visible": { "eq": ["query.mode", "edit"] }
    },
    "email_view": {
      "type": "DescriptionList",
      "props": {
        "items": [{ "label": "Email", "value": { "$data": "/user/email" } }]
      },
      "visible": { "ne": ["query.mode", "edit"] }
    },
    "email_edit": {
      "type": "Input",
      "props": {
        "field": "email",
        "label": "Email Address",
        "input_type": "email",
        "data_path": "/user/email"
      },
      "visible": { "eq": ["query.mode", "edit"] }
    },
    "save_btn": {
      "type": "Button",
      "props": { "label": "Save", "button_type": "submit" },
      "visible": { "eq": ["query.mode", "edit"] }
    }
  }
}

The visible condition { "eq": ["query.mode", "edit"] } shows the element only when ?mode=edit is present in the URL. The inverse { "ne": ["query.mode", "edit"] } shows the element in all other cases (view mode). See Data Binding & Visibility for the full visible condition reference.