Component type catalog

Every built-in type and the fields it accepts. Common fields (id, bind, visible, enabled, readOnly, required) are described once in View definition & components; this page lists what each type adds.


Component Type Catalog

Input fields

All input types support bind, label, visible, enabled, readOnly, required, placeholder, helperText, tooltip, and onChange.

textField

Single-line text input.

{ "id": "nameField", "type": "textField", "label": "Full Name", "bind": "$.name" }

textAreaField

Multi-line text input.

Extra field Description
rows number of visible rows (integer)
{
  "id": "detailsField", "type": "textAreaField",
  "label": "Details", "bind": "$.details",
  "rows": 4, "placeholder": "Describe the issue..."
}

numericField

Number input. Min/max constraints are read from the JSON Schema minimum / maximum properties of the bound field.

{ "id": "qtyField", "type": "numericField", "label": "Quantity", "bind": "$.qty" }

passwordField

Masked text input.

{ "id": "pwdField", "type": "passwordField", "label": "Password", "bind": "$.password" }

emailField

Email input with built-in format validation.

{ "id": "emailField", "type": "emailField", "label": "Email", "bind": "$.email",
  "placeholder": "you@example.com" }

checkboxField

Boolean checkbox.

{ "id": "termsField", "type": "checkboxField",
  "label": "I agree to the terms", "bind": "$.termsAccepted" }

toggleField

Boolean switch / toggle.

{ "id": "darkMode", "type": "toggleField", "label": "Dark mode", "bind": "$.darkMode" }

selectField

Dropdown. Options come from one of three sources: static list, optionsExpr, or optionsUrl. All three are resolved client-side by the bundled React renderer — the server-side ViewEvaluator passes options through unchanged and never reads optionsExpr/optionsUrl/ optionsPath, so a raw GET /models/{id}/view response (or an MCP/console/custom-renderer consumer) sees only the static options list, not resolved dynamic options.

Extra field Description
options Static [{value, label}] list
optionsExpr JSONata expression producing [{value, label}] from model state (client-evaluated)
optionsUrl URL returning a JSON array (fetched client-side)
optionsPath JSON path into the optionsUrl response to find the [{value,label}] array
{
  "id": "categoryField", "type": "selectField",
  "label": "Category", "bind": "$.category",
  "options": [
    { "value": "product",  "label": "Product" },
    { "value": "service",  "label": "Service" },
    { "value": "shipping", "label": "Shipping" }
  ]
}

radioField

Radio button group. Accepts the same options / optionsExpr fields as selectField.

{
  "id": "ratingField", "type": "radioField",
  "label": "Overall Satisfaction", "bind": "$.overallRating",
  "options": [
    { "value": "1", "label": "1 — Very dissatisfied" },
    { "value": "5", "label": "5 — Very satisfied" }
  ]
}

multiSelectField

Multiple-choice dropdown. Accepts the same options / optionsExpr / optionsUrl / optionsPath fields as selectField.

{ "id": "tagsField", "type": "multiSelectField", "label": "Tags", "bind": "$.tags",
  "optionsUrl": "/api/tags", "optionsPath": "$.items" }

dateField

Date picker (date only, no time).

{ "id": "dobField", "type": "dateField", "label": "Date of Birth", "bind": "$.dob" }

dateTimeField

Combined date and time picker.

{ "id": "startField", "type": "dateTimeField", "label": "Start Time", "bind": "$.startAt" }

timeField

Time-only picker (HH:mm).

{ "id": "startTimeField", "type": "timeField", "label": "Start Time", "bind": "$.startTime" }

sliderField

Range slider. Mutation is sent on blur/debounce, not on every drag step.

Extra field Description
min Range minimum
max Range maximum
step Step increment
{ "id": "volumeField", "type": "sliderField", "label": "Volume",
  "bind": "$.volume", "min": 0, "max": 100, "step": 5 }

fileUploadField

Uploads a binary file to POST /blobs and stores the resulting BlobRef ({$blobId, $mediaType, $bytes}) in the bound field.

Extra field Description
accept MIME type filter, e.g. "image/*"
multiple Allow multiple files
minFiles / maxFiles File-count bounds (also resolvable from a minItems/maxItems metaDerivation on the bind path)
minSize / maxSize Per-file byte-size bounds (also resolvable from a minSize/maxSize metaDerivation)
allowedMediaTypes Allowed media types (also resolvable from an allowedMediaTypes metaDerivation)
{ "id": "attachmentField", "type": "fileUploadField", "label": "Attachment",
  "bind": "$.attachment", "accept": "image/*" }

countrySelector

Country dropdown pre-populated from restcountries.com. No extra configuration required.

{ "id": "countryField", "type": "countrySelector", "label": "Country", "bind": "$.country" }

countryRegionSelector

Region / state / province selector. Depends on the country selected by a sibling countrySelector. The dependsOn field must point to the bind path of the sibling.

Extra field Description
dependsOn Bind path of the countrySelector this component reads the country code from
{
  "id": "regionField", "type": "countryRegionSelector",
  "label": "State / Region", "bind": "$.region",
  "dependsOn": "$.country"
}

phoneNumberField

International phone number input with a built-in country code picker (IDD dial codes from restcountries.com).

{ "id": "phoneField", "type": "phoneNumberField", "label": "Phone", "bind": "$.phone" }

currencyField / percentField

numericField plus a display convention. The bound value stays a plain number — these change what the input shows beside the box, never what the model stores, so swapping one spelling for another cannot alter a derivation or a constraint.

Extra field Description
format "currency", "percent", "value"; defaults from the type
currency ISO-4217 code ("EUR"); read only when the format is currency

percent appends a % and does not rescale: a stored 7.5 shows as 7.5%. A model whose derivation already computes rate * 100 would otherwise display 750%.

{ "id": "premiumField", "type": "currencyField", "label": "Annual premium",
  "bind": "$.premium", "currency": "EUR" }

richTextField

textAreaField with a markdown toolbar. The value stored is markdown, not HTML — so it stays something a derivation can read, a constraint can measure and a diff can show, with no sanitising step between the model and storage.

Extra field Description
rows Visible rows (integer)
toolbar "basic" (default), "full", or "none"

autocompleteField / comboBox

A selectField that filters as you type — what a select degrades into past a few dozen options. Same options / optionsExpr / optionsUrl / optionsPath contract.

Extra field Description
allowCustom Accept a value outside options. Defaults true for comboBox, false otherwise

allowCustom is a renderer affordance, not a validation rule: the schema still decides what the model accepts, so a custom value can be rejected on mutate like any other.

{ "id": "cityField", "type": "autocompleteField", "label": "City",
  "bind": "$.city", "optionsExpr": "cities.{ 'value': code, 'label': name }" }

tagsField

An array of scalars edited as chips. Writes the whole array at the bound path, so derivations see one change per edit rather than one per element. Before this, a string[] needed a full sectionList.

Extra field Description
options Optional suggestions; allowCustom defaults true
{ "id": "tagsField", "type": "tagsField", "label": "Tags", "bind": "$.tags" }

dateRangeField

Two coupled dates written to two separate paths.

Extra field Description
bindFrom / bindTo Paths for the two ends — these carry the value, not bind
fromLabel / toLabel Captions (default "From" / "To")
minDate / maxDate Picker bounds (authored hints)

Two paths rather than one object bind, because the ends almost always already have their own derivations and constraints — endDate >= startDate is a constraint, not a widget rule. Each end mutates independently so the audit record shows which one the user actually changed. bind, if set, is only the meta-inheritance anchor.

{
  "id": "coverPeriod", "type": "dateRangeField", "label": "Cover period",
  "bindFrom": "$.startDate", "bindTo": "$.endDate", "minDate": "2026-01-01"
}

ratingField / numericStepper

The same min / max / step as sliderField, drawn as stars or as −/+ buttons.

Extra field Description
min / max / step Range and increment (ratingField defaults to 1–5 step 1)

Unlike the slider these commit on click: there is no drag to wait out, so dependent derivations recompute immediately.

{ "id": "satisfaction", "type": "ratingField", "label": "How satisfied?",
  "bind": "$.rating", "min": 1, "max": 5 }

Data output

Where a display component gets its value. Each type below reads its value from either a bind (a $.path) or an expression field (text / value / caption / delta). The expression fields obey the $ rule: the server evaluates them as JSONata only when the string contains a $ — a bare text: "myField" is shown literally. Prefer bind for a stored or derived value; use a $-bearing expression ($string(field), $round(field,1)) when the value is computed inline. See Field value kinds for the full rule and the server-vs-UI note.

label

Displays a bound value or a dynamic text expression next to an optional label caption.

Extra field Description
text JSONata expression or literal; overrides the bound value for display. Subject to the $ rule — evaluated only when it contains a $ (see the note above); a bare field name is shown literally
format "currency", "percent", "number", "integer" — formats a bound numeric value the same way dataTable columns, keyValueList rows and statTile do. Omit it and the value renders verbatim, so a label over an id, a year or free text is unaffected
currency ISO-4217 code, read only when format is currency
{
  "id": "totalLabel", "type": "label",
  "label": "Grand Total", "bind": "$.total",
  "format": "currency", "currency": "USD"
}
{
  "id": "greetingLabel", "type": "label",
  "text": "'Hello, ' & $string(name) & '!'"
}

staticText

Non-reactive text block. text is a literal or a JSONata expression; no binding.

Extra field Description
text Literal or JSONata expression
format "markdown" (default), "text", or "html"

markdown is the default because it is safe: it escapes the source before applying light formatting, and a staticText is often bound (via a JSONata text) to model state — which, since Valem has no per-field access control, a low-trust mutator can set. It is also the read half of richTextField, applying the subset that field’s toolbar produces, so a stored <script> renders as visible text rather than executing. text shows the content verbatim (escaped, no formatting). html injects its content unescaped and must be opted into explicitly — use it only for authored, trusted content.

Changed default. Before the catalog expansion staticText always rendered as raw HTML. Unset format now means markdown; a spec that relied on raw <tag> markup must set "format": "html".

{ "id": "intro", "type": "staticText",
  "text": "Please fill in the form below. All fields marked * are required." }
{ "id": "notesOut", "type": "staticText", "format": "markdown", "text": "$.notes" }

badge

Status indicator chip.

Extra field Description
text JSONata expression or literal for the badge label. A badge has no bind, so to show a field’s value put it in text — subject to the $ rule, so reference the field through a $ function ($string(field)) rather than bare
variant "primary", "secondary", "success", "warning", "danger"
{
  "id": "statusBadge", "type": "badge", "label": "Priority",
  "text":    "$boolean(priorityFlag) ? 'Needs follow-up' : 'Normal'",
  "variant": "danger"
}

Known gap: unlike text, variant is a plain string on ComponentSpec — the server-side ViewEvaluator passes it through unevaluated, it is not JSONata-capable server-side. The bundled React renderer masks this by re-evaluating variant as JSONata client-side, so a JSONata variant expression only resolves correctly through the built-in UI, not through the raw GET /models/{id}/view (or MCP/console) response.

separatorLine

Horizontal rule / divider. No extra fields.

{ "id": "sep1", "type": "separatorLine" }

progressBar

Numeric value rendered as a filled bar.

Extra field Description
min Range minimum
max Range maximum
showValue Display a numeric label alongside the bar
format "percent" (default) or "value" (renders 75 / 100)
{ "id": "uploadProgress", "type": "progressBar", "label": "Upload",
  "bind": "$.uploadPercent", "min": 0, "max": 100, "format": "percent" }

dataTable

Tabular view of an array field.

Extra field Description
tableColumns Array of ColumnSpec objects defining column layout
pageSize Rows per page (integer); omit for no pagination
{
  "id": "itemsTable", "type": "dataTable",
  "label": "Order Items", "bind": "$.items",
  "tableColumns": [
    { "field": "name",      "header": "Product",    "width": "40%" },
    { "field": "qty",       "header": "Qty",        "width": "15%" },
    { "field": "price",     "header": "Unit Price", "format": "currency", "width": "20%" },
    { "field": "lineTotal", "header": "Line Total", "format": "currency", "width": "25%" }
  ],
  "pageSize": 20
}

ColumnSpec

Field Description
field Property name within each array element
header Column header label
format "currency", "percent", "number", "integer"
currency ISO-4217 code. Per column, since a table legitimately holds more than one; without it a currency format falls back to the renderer’s default
width CSS width string (e.g. "40%", "120px")

Formatting is shared with keyValueList, statTile and label, so a column, a summary row, a tile and a label over the same field always read the same. In particular percent appends a sign and does not rescale — a stored 7.5 shows as 7.5%, not 750%. Currency uses the short symbol ($1,250.00, not US$1,250.00).

dataChart

Chart backed by Recharts.

Extra field Description
chartType "bar", "line", "area", "pie"
chartX Property name used for the X axis (category key)
chartSeries Array of ChartSeriesSpec objects
{
  "id": "salesChart", "type": "dataChart",
  "label": "Monthly Sales", "bind": "$.monthlySales",
  "chartType": "bar",
  "chartX": "month",
  "chartSeries": [
    { "field": "revenue", "label": "Revenue", "color": "#4f46e5" },
    { "field": "cost",    "label": "Cost",    "color": "#ef4444" }
  ]
}

ChartSeriesSpec

Field Description
field Property name in each data point object
label Legend label
color CSS color string

alert / callout

The block-level spelling of badge: same label (heading), text (body) and variant (severity). danger and warning render as role="alert", so the problem is announced and not only coloured.

{ "id": "underwritingNotice", "type": "alert", "variant": "warning",
  "label": "Referral required", "text": "$.premium > $const.autoApproveLimit ? 'Manual review' : ''" }

spacer

separatorLine without the rule.

Extra field Description
size Gap in pixels (default 16)

image

Extra field Description
src Literal URL or JSONata expression; falls back to the bound value
alt Alternative text — supply it; an image without one is unreadable to a screen reader
width / height / fit CSS sizing and object-fit

A bound value is usually what a fileUploadField wrote — a BlobRef of shape {$blobId, $mediaType, $bytes} — which the renderer resolves to /blobs/{id}. That makes an upload and its preview two components over one path.

Extra field Description
href Literal URL or JSONata expression; falls back to the bound value
text Link caption (literal or expression); defaults to label, then href
target "_blank" for a new tab — rel="noopener noreferrer" is added automatically
icon Optional leading icon

For navigation inside the model use a button or menu with a navigate handler, which switches view without leaving the page or discarding unsaved edits.

sparkline

A dataChart stripped of axes, grid, legend and tooltip so it can sit inline beside a number. Same bind / chartType / chartSeries; only the first series is drawn — a sparkline with two series needs a legend, and once it has one it is a dataChart.

gauge

A progressBar drawn as a 180° arc. Same min, max, showValue, format.

keyValueList / summaryList

A read-only caption/value list — the review step of a form, and the shape a derivation-heavy model most often ends on. Rows come from items, not from bind.

Extra field Description
items Array of KeyValueItemSpec
columns Caption/value column pairs per row (default 1)

KeyValueItemSpec

Field Description
label Row caption
bind Path to read the value from (the usual choice)
text JSONata expression, for a row with no single path behind it. Subject to the $ rule — needs a $ to evaluate; a plain string is shown literally
format "currency", "percent", "number", "integer"
currency ISO-4217 code. Per row, because a summary legitimately mixes them — a quoted price beside its converted equivalent. A currency format with no code falls back to the renderer’s default

When both bind and text are set, bind wins — a row never shows two different things depending on which field the reader looks at. Rows are resolved server-side, so a non-browser consumer of GET /models/{id}/view gets a printable summary without evaluating anything.

{
  "id": "quoteSummary", "type": "summaryList", "label": "Your quote",
  "items": [
    { "label": "Annual premium", "bind": "$.premium", "format": "currency" },
    { "label": "Excess",         "bind": "$.excess",  "format": "currency" },
    { "label": "Reference",      "bind": "$.quoteRef" }
  ]
}

statTile / metric

One headline number with its supporting text.

Extra field Description
bind Path to the number (the usual choice), or …
value … a JSONata expression, when no single path holds it. Subject to the $ rule — needs a $ to evaluate (see the Data output note); a plain string is shown literally
delta Expression for the movement line ($ rule applies)
caption Expression for the sub-caption ($ rule applies); write fixed text plainly, e.g. "per month"
trend "up", "down", "flat", or an expression producing one ($ rule applies)
format / currency As currencyField
variant, icon, tooltip Presentation

trend is authored separately from the sign of delta on purpose: whether a rising number is good news is a domain question — spend up is bad, savings up is good — that only the spec knows. Unset means the delta is shown without a verdict.

jsonViewer

The bound subtree as formatted JSON. Bind to $ for the whole merged document — base fields with derived values already spliced in, which is what expressions actually evaluate against.

Extra field Description
collapsed Initial fold state (bool or expression)
maxDepth Truncate deeper nodes to

explainPanel / auditTimeline

Valem’s two path-scoped record trails: explainPanel shows why a field holds its value (GET /models/{id}/explain/{path} — the live derivation and constraint traces), auditTimeline shows how it got there (GET /models/{id}/audit — the durable, hash-chained record of committed cycles).

Extra field Description
bind Path to explain; for auditTimeline a path prefix, unset means the whole model
limit Maximum rows (default 20)
showConstraints Include constraint traces as well as derivations
collapsed Initial fold state

The server sends the declaration, not the rows. ViewEvaluator has no access to the trace ring buffer or the audit store, and giving it one would put an unbounded read inside every view evaluation and every viewDelta; the renderer fetches the rows itself, the same division of labour as optionsUrl. A consumer of the raw GET /models/{id}/view response therefore gets the declaration only.

validationSummary

Every flagged constraint in one block, each linking to the field it is about.

Extra field Description
pathPrefix Narrow to one section; unset means the whole model
variant "danger" (default), "warning", "info"
maxItems Cap the list
emptyText Shown when there are none; omit to render nothing at all

This exists because a flag-policy constraint commits. It records a violation and lets the model stay editable while temporarily inconsistent — the point of the policy — but leaves the violation with nowhere to appear except beside a field bound to the same path, and a constraint spanning three fields is beside none of them. (A rollback constraint needs none of this: it rejects the mutation and surfaces as an error on the failed call.)

Like the trace panels, the violations reach the renderer through its own violations map, not through the view.

effectStatus

An effect’s statusPath machine, drawn: pending → in_flight → applied | failed.

Extra field Description
bind The effect’s statusPath
effectId The effect this reports on (documentation for the reader)
errorPath Path holding the failure message
showRetry Show a retry control (defaults to true when failed)
retryLabel Retry button caption
onRetry EventHandler — typically resets the status to pending

The status is an ordinary model field, so it arrives through the same viewDelta as everything else and needs no polling. Resetting statusPath re-arms the dedupe guard, which is what makes the engine re-fire the effect — the component never dispatches one itself, because a view can only ask the model to.

Without this the only way to show an in-flight effect is a badge with a JSONata variant, which the server passes through unevaluated — so it renders correctly in the bundled UI and nowhere else.

{
  "id": "quoteEffect", "type": "effectStatus", "label": "Underwriting",
  "bind": "$.quote.status", "errorPath": "$.quote.error", "effectId": "fetchQuote",
  "onRetry": { "mutations": "{ '$.quote.status': 'pending' }" }
}

Aggregates

group

Layout container. Components inside are rendered according to the layout setting.

Extra field Description
layout "vertical", "horizontal", "grid"
columns Column count for "grid" layout
components Nested ComponentSpec array
{
  "id": "summaryGroup", "type": "group",
  "layout": "horizontal",
  "components": [
    { "id": "scoreLabel", "type": "label", "label": "Score", "bind": "$.score" },
    { "id": "statusBadge", "type": "badge", "text": "status" }
  ]
}

fieldSet

HTML <fieldset> with a legend caption. Functionally equivalent to group.

Extra field Description
legend Text displayed as the fieldset legend
components Nested ComponentSpec array
{
  "id": "contactGroup", "type": "fieldSet",
  "legend": "Contact Information",
  "components": [
    { "id": "emailField", "type": "emailField", "bind": "$.email" }
  ]
}

sectionList

Displays an array field with add/remove controls. Each item is edited in a separate view identified by itemView.

Extra field Description
itemView id of the ViewSpec used to edit a single array element
canAdd bool or JSONata expression controlling whether Add is available
canRemove bool or JSONata expression controlling whether Remove is available
addLabel Label for the Add button (default "Add")
removeLabel Label for the Remove button (default "Remove")
{
  "id": "itemsList", "type": "sectionList",
  "label": "Order Items", "bind": "$.items",
  "itemView": "item-editor",
  "canAdd": true, "canRemove": true,
  "addLabel": "Add Item", "removeLabel": "Remove"
}

sectionItem

Single-element editor for use inside a view named by a sectionList’s itemView. The bind points to the specific array element path; components describes its fields.

Extra field Description
components Nested ComponentSpec array for the element’s fields
{
  "id": "itemEditor", "type": "sectionItem",
  "bind": "$.items[0]",
  "components": [
    { "id": "nameField",  "type": "textField",    "label": "Product Name", "bind": "$.items[0].name" },
    { "id": "priceField", "type": "numericField",  "label": "Price",        "bind": "$.items[0].price" }
  ]
}

card

A group on a titled surface; label is the heading. Accepts the same layout, columns and components.

toolbar / buttonGroup

A row of actions. They differ only in spacing — a toolbar separates its children, a buttonGroup butts them into one control. Neither honours layout: a vertical toolbar is a menu.

tabs / tabItem

tabs gives each child its own panel, captioned by that child’s label. The children are ordinary containers — tabItem reads best, but any container works — so a tab body is authored exactly like a group.

{
  "id": "quoteTabs", "type": "tabs",
  "components": [
    { "id": "coverTab", "type": "tabItem", "label": "Cover",  "components": [] },
    { "id": "driverTab", "type": "tabItem", "label": "Driver", "components": [] }
  ]
}

The active tab is local UI state and deliberately not in the model — it is not something a derivation, a constraint or an audit record should have an opinion about. When the position must survive a reload, use separate views and navigate between them; the active view id is then the step, and a stepper or breadcrumb renders it.

collapsible / accordion

collapsible is one foldable panel with label as its header; accordion is a bordered stack of them.

Extra field Description
collapsed Initial fold state (bool or expression)

collapsed seeds the state and is not a live binding — the renderer owns it from then on. “Starts closed until the user picks a plan” is expressible; “force closed whenever X” is not, and would mean a section slamming shut mid-edit. Use visible for that.

The accordion does not coordinate its children: each collapsible owns its own state, so several can be open at once. Single-open behaviour would mean the accordion closing a panel the user just opened.


Actions

button

Clickable button.

Extra field Description
variant String or JSONata: "primary", "secondary", "danger", "ghost"
icon Icon name string (renderer-specific)
onClick EventHandler fired on click
{
  "id": "submitBtn", "type": "button",
  "label": "Submit", "variant": "primary",
  "onClick": {
    "mutations": "{'$.status': 'submitted'}",
    "navigate": "confirmation"
  }
}

Navigation menu linking to other views.

Extra field Description
menuItems Array of MenuItemSpec objects
orientation "horizontal" or "vertical"
{
  "id": "sideNav", "type": "menu",
  "orientation": "vertical",
  "menuItems": [
    { "label": "Overview",  "targetView": "overview" },
    { "label": "Details",   "targetView": "detail",  "icon": "list" },
    { "label": "Charts",    "targetView": "charts",  "icon": "chart" }
  ]
}

MenuItemSpec

Field Description
label Display text
targetView id of the ViewSpec to navigate to
icon Optional icon name (renderer-specific)

stepper / breadcrumb

The same menuItems as a menu, drawn as a numbered progression or a trail. Same orientation.

None of the three holds a position of its own: “which step” is the active view id, so it survives a reload, can be driven by a button’s navigate handler, and is what onNavigate reports to the host app. A stepper tracking its own index would disagree with the view on screen the moment anything else navigated.

Steps before the active one are marked done — a positional claim, not a claim about validity. The model’s constraints decide whether a step’s data is actually acceptable.

{
  "id": "quoteSteps", "type": "stepper",
  "menuItems": [
    { "label": "Vehicle", "targetView": "vehicle" },
    { "label": "Driver",  "targetView": "driver" },
    { "label": "Quote",   "targetView": "quote" }
  ]
}