| title | Formspec Component Specification |
|---|---|
| version | 1.0.0-draft.1 |
| date | 2026-04-09 |
| status | draft |
This document is a Draft companion specification to the Formspec v1.0 Core Specification. It defines the Formspec Component Document format — a sidecar JSON document that describes a parallel presentation tree of UI components bound to a Formspec Definition's items.
Status: Draft Companion Specification Version: 1.0.0 Date: 2025-01-14 Depends on: Formspec Core Specification v1.0 (spec.md), Formspec Theme Specification v1.0 (theme-spec.md), FEL Normative Grammar v1.0 (fel-grammar.md)
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
JSON syntax and data types are as defined in [RFC 8259]. JSON Pointer syntax is as defined in [RFC 6901]. URI syntax is as defined in [RFC 3986].
JSON examples use // comments for annotation; comments are not valid
JSON. Property names in monospace (component) refer to JSON keys.
Section references (§N) refer to this document unless prefixed with
"core" (e.g., "core §4.2.5") or "theme" (e.g., "theme §3").
- This document defines Tier 3 Component Documents for explicit, tree-based Formspec rendering.
- A valid component document requires
$formspecComponent,version,targetDefinition, andtree. - Component trees control layout and widget selection but cannot override core behavioral semantics from the Definition.
- This BLUF is governed by
schemas/component.schema.json; generated schema references are the canonical structural contract.
- Bottom Line Up Front
- §1 Introduction
- §2 Document Structure
- §3 Component Model
- §4 Slot Binding
- §5 Built-In Components — Core (18)
- §6 Built-In Components — Progressive (17)
- §7 Custom Components
- §8 Conditional Rendering
- §9 Responsive Design
- §10 Theming and Design Tokens
- §11 Cross-Tier Interaction
- §12 Validation and Conformance
- §13 Complexity Controls
- Appendix A: Full Example — Budget Form
- Appendix B: Component Quick Reference
- Appendix C: DataType ↔ Component Compatibility
The Formspec Core Specification defines what data to collect (Items, core §4.2) and how it behaves (Binds, Shapes). The Formspec Theme Specification (theme-spec.md) defines Tier 2 presentation: a selector cascade, design tokens, widget configuration, and page layout that control how a Definition is rendered.
This specification defines Tier 3 of the Formspec presentation model: a Component Document that describes a complete, parallel presentation tree of UI components. Where Tier 2 maps items to widgets one-to-one via selectors, Tier 3 builds an explicit tree of layout containers, input controls, and display elements, with each input component slot-bound to a Definition item by key.
A Component Document:
- References a Definition by URL (same binding mechanism as Tier 2 themes).
- Declares a single root component whose subtree defines the full visual layout of the form.
- Binds input components to Definition items, inheriting labels, validation rules, required state, and relevance from the Definition.
- Uses FEL expressions for conditional rendering (
whenproperty). - Supports responsive breakpoint overrides and design tokens.
- Defines a fixed catalog of 35 built-in components (18 Core + 17 Progressive) plus a custom component registry for reuse.
Multiple Component Documents MAY target the same Definition. This enables platform-specific presentations (web wizard, mobile single-page, print layout) without modifying the Definition or its behavioral rules.
The Formspec architecture defines a three-tier presentation model:
| Tier | Concern | Defined In |
|---|---|---|
| 1. Structure hints | Advisory widget hints on Items | Core §4.2.5 (presentation) |
| 2. Theme | Selector cascade, tokens, layout grid | Theme Spec (theme-spec.md) |
| 3. Component | Explicit component tree with slot binding | This specification |
Tier 3 is the most expressive layer. When a Component Document is applied, it takes precedence over both Tier 2 themes and Tier 1 inline hints for layout and widget selection. However, the Definition's behavioral rules (required, relevant, readonly, constraint, calculate) always govern data semantics — Tier 3 cannot override them.
Tier 3 MAY coexist with a Tier 2 theme. When both are present:
- The Component Document controls layout and component selection.
- Theme tokens are available as
$token.references within the Component Document (§10.3). - Items not explicitly bound in the component tree fall back to Tier 2/Tier 1 rendering (§11.1).
FEL expressions (fel-grammar.md) are used in the when property for
conditional rendering (§8). FEL is NOT used for computed props, data
transformation, or any purpose other than boolean visibility conditions
and display text interpolation.
This specification defines two conformance levels:
| Level | Components | Requirement |
|---|---|---|
| Core Conformant | 18 Core components (§5) | MUST support all 18 Core components. MUST apply fallback rules (§6.18) when encountering Progressive components. |
| Complete Conformant | All 35 components (§5 + §6) | MUST support all 18 Core components and all 17 Progressive components. |
A processor that claims Core conformance MUST, upon encountering a Progressive component, substitute the specified Core fallback (§6.18) and SHOULD emit an informative warning.
A processor that claims Complete conformance MUST render all 35 built-in components natively.
Both levels MUST support the custom component mechanism (§7).
| Term | Definition |
|---|---|
| Definition | A Formspec Definition document (core spec §4). |
| Component Document | A Formspec Component document conforming to this specification. |
| Component | A node in the component tree. Each component has a type, optional binding, and optional children. |
| Tier 1 hints | The formPresentation and presentation properties defined in core spec §4.1.1 and §4.2.5. |
| Tier 2 theme | A Formspec Theme Document conforming to theme-spec.md. |
| Tier 3 component | A Component Document conforming to this specification. |
| Renderer | Software that presents a Definition to end users using a Component Document. |
| Token | A named design value (color, spacing, typography) defined in the tokens map. |
| Slot binding | The association between a component and a Definition item via the bind property. |
| Core component | One of the 17 components that all conforming processors MUST support. |
| Progressive component | One of the 16 additional components that Complete processors MUST support, with defined fallbacks for Core processors. |
| Custom component | A reusable component template defined in the components registry. |
A Formspec Component Document is a JSON object. Conforming implementations MUST recognize the following top-level properties and MUST reject any Component Document that omits a REQUIRED property.
{
"$formspecComponent": "1.0",
"url": "https://agency.gov/forms/budget/components/wizard",
"version": "1.0.0",
"targetDefinition": {
"url": "https://agency.gov/forms/budget",
"compatibleVersions": ">=1.0.0 <2.0.0"
},
"breakpoints": {
"sm": 576,
"md": 768,
"lg": 1024
},
"tokens": {},
"components": {},
"tree": {}
}| Pointer | Field | Type | Required | Notes | Description |
|---|---|---|---|---|---|
#/properties/$formspecComponent |
$formspecComponent |
string |
yes | const: "1.0"; critical |
Component specification version. MUST be '1.0'. |
#/properties/breakpoints |
breakpoints |
$ref |
no | $ref: #/$defs/Breakpoints |
Named viewport breakpoints for responsive prop overrides. Keys are breakpoint names; values are minimum viewport widths in pixels. Mobile-first cascade: base props apply to all widths, then overrides merge in ascending order. |
#/properties/components |
components |
object |
no | — | Registry of custom component templates. Keys are PascalCase names (MUST NOT collide with built-in names). Each template has params and a tree that is instantiated with {param} interpolation. |
#/properties/description |
description |
string |
no | — | Human-readable description. |
#/properties/extensions |
extensions |
object |
no | — | Document-level extension properties. All keys MUST be prefixed with 'x-'. |
#/properties/name |
name |
string |
no | — | Machine-friendly short identifier. |
#/properties/targetDefinition |
targetDefinition |
$ref |
yes | $ref: #/$defs/TargetDefinition; critical |
Binding to the target Formspec Definition and optional compatibility range. |
#/properties/title |
title |
string |
no | — | Human-readable name. |
#/properties/tokens |
tokens |
$ref |
no | $ref: #/$defs/Tokens |
Flat key-value map of design tokens. Referenced in style objects and token-able props via $token.key syntax. Tier 3 tokens override Tier 2 theme tokens of the same key. |
#/properties/tree |
tree |
$ref |
yes | $ref: #/$defs/AnyComponent; critical |
Root component node of the presentation tree. MUST be a single component object (wrap multiple children in Stack or Page). |
#/properties/url |
url |
string |
no | — | Canonical URI identifier for this Component Document. |
#/properties/version |
version |
string |
yes | critical | Version of this Component Document. |
The generated table above is the canonical structural contract for Component Document top-level properties.
Processors MUST ignore unrecognized top-level properties whose keys begin
with x-. Processors MUST reject unrecognized top-level properties that
do NOT begin with x-.
The targetDefinition object binds this Component Document to a specific
Definition.
| Property | Type | Cardinality | Description |
|---|---|---|---|
url |
string (URI) | 1..1 (REQUIRED) | Canonical URL of the target Definition (url property from the Definition). |
compatibleVersions |
string | 0..1 (OPTIONAL) | Semver range expression (e.g., ">=1.0.0 <2.0.0") describing which Definition versions this Component Document supports. When absent, the document is assumed compatible with any version. |
When compatibleVersions is present, a processor SHOULD verify that the
Definition's version satisfies the range before applying the component
tree. A processor MUST NOT fail if the range is unsatisfied; it SHOULD
warn and MAY fall back to Tier 2/Tier 1 rendering.
The binding mechanism is identical to the Theme Specification's
targetDefinition (theme-spec §2.2). A Component Document and a Theme
Document MAY target the same Definition simultaneously.
Formspec Component Documents SHOULD use the file extension
.formspec-component.json.
When served over HTTP, the content type SHOULD be
application/json. Processors MAY recognize the custom media type
application/formspec-component+json when registered.
The file extension convention enables tooling to distinguish Component
Documents from Definition documents (.formspec.json) and Theme
Documents (.formspec-theme.json) by filename alone.
The following is the smallest valid Component Document:
{
"$formspecComponent": "1.0",
"version": "1.0.0",
"targetDefinition": {
"url": "https://example.com/form"
},
"tree": {
"component": "Stack",
"children": [
{ "component": "TextInput", "bind": "name" }
]
}
}This document:
- Declares
$formspecComponentversion"1.0"(REQUIRED). - Provides a
versionfor the document itself (REQUIRED). - Binds to a target Definition via
targetDefinition(REQUIRED). - Defines a root
treewith a singleStackcontaining oneTextInputbound to the item key"name"(REQUIRED).
All other top-level properties (url, name, title, description,
breakpoints, tokens, components) are OPTIONAL and default to
empty/absent.
A component tree is a hierarchical structure of component objects. Each component object describes a single UI element — a layout container, an input control, a display element, or a structural grouping.
Every component object is a JSON object. The following base properties are recognized on all component objects:
| Property | Type | Cardinality | Description |
|---|---|---|---|
component |
string | 1..1 (REQUIRED) | The component type name. MUST be a built-in component name (§5, §6) or a key in the components registry (§7). |
id |
string | 0..1 (OPTIONAL) | Unique identifier within the component tree document. See below. |
bind |
string | 0..1 (varies) | Item key from the Definition. See §4 for rules per component category. |
when |
string (FEL) | 0..1 (OPTIONAL) | FEL boolean expression for conditional rendering. See §8. |
responsive |
object | 0..1 (OPTIONAL) | Breakpoint-keyed prop overrides. See §9. |
style |
object | 0..1 (OPTIONAL) | Flat style map. Values MAY contain $token.path references. See §10.2. |
cssClass |
string | array of strings | 0..1 (OPTIONAL) | CSS class name(s) that web renderers SHOULD apply to the component's root element. Additive to renderer-generated classes. Non-web renderers MAY ignore. Values MAY contain $token. references. |
accessibility |
AccessibilityBlock | 0..1 (OPTIONAL) | Accessibility overrides applied to the component's root element. See §3.5. |
children |
array | 0..1 (varies) | Array of child component objects. Only components that accept children (§3.4) MAY include this property. |
Components MAY include an id property — a unique string identifier
within the component tree document. The id MUST match the pattern
^[a-zA-Z][a-zA-Z0-9_\-]*$ (letters, digits, underscores, hyphens;
must start with a letter).
When present, id MUST be unique across the entire component tree
document. The id enables:
- Locale string addressing:
$component.<id>.<prop>keys in Locale Documents (locale spec §3.1.8). - Test selectors: Stable identifiers for automated testing.
- Accessibility anchoring: Stable references for assistive technology integration.
Processors MUST validate id uniqueness when both id values and
the component tree are available:
- At validation/linting time, an
idcollision MUST produce an error. - At runtime, an
idcollision SHOULD produce a warning and the processor MUST bind only the first occurrence in document order (matching the editable binding uniqueness pattern in §4.3).
When a component node with id appears inside a repeat template
(as a child of a DataTable bound to a repeatable group per §6.14, or
an Accordion per §6.3), the id identifies the template node,
not individual rendered instances. All rendered instances of the
template share the same id. This is consistent with the
template-instantiation rendering model defined in §4.4 — the
component tree document contains one subtree that the renderer
instantiates N times.
For locale addressing, this means a single locale key
$component.<id>.<prop> applies to all repeat instances. If
per-instance text is needed, the locale string value may use FEL
interpolation with @index and @count (evaluated in each repeat
instance's binding scope).
In addition to these base properties, each component type defines its own component-specific props (documented in §5 and §6). Component-specific props are siblings of the base properties in the same JSON object.
Example of a fully-specified component object:
{
"component": "TextInput",
"bind": "projectName",
"when": "$hasProject = true",
"placeholder": "Enter project name",
"style": {
"borderColor": "$token.color.primary"
},
"responsive": {
"sm": { "placeholder": "Project" }
}
}Processors MUST ignore unrecognized properties on component objects. This enables forward-compatible extension.
The tree property MUST contain exactly one component object. This
object is the root of the component tree.
To present multiple components at the top level, authors MUST wrap them
in a layout component (typically Stack or Page):
// ✗ INVALID — tree cannot be an array
"tree": [
{ "component": "TextInput", "bind": "name" },
{ "component": "TextInput", "bind": "email" }
]
// ✓ VALID — single root wrapping multiple children
"tree": {
"component": "Stack",
"children": [
{ "component": "TextInput", "bind": "name" },
{ "component": "TextInput", "bind": "email" }
]
}The root component MAY be any component that accepts children (Layout or Container). A root that is a leaf component (Input or Display) is valid but yields a form with a single element.
The children array defines an ordered list. Renderers MUST
preserve the array order when rendering:
- The first child in the array MUST be rendered first (topmost in a vertical stack, leftmost in a horizontal layout, first column in a grid).
- Subsequent children MUST follow in array order.
Renderers MUST NOT reorder children unless explicitly instructed by a responsive override (§9) that changes layout properties (e.g., switching a Stack's direction), but even then the logical array order is preserved within the new layout direction.
Components are classified into four categories. Nesting rules depend on the category:
| Category | Accepts children |
Examples |
|---|---|---|
| Layout | Yes | Page, Stack, Grid, Columns, Tabs, Accordion |
| Container | Yes | Card, Collapsible, ConditionalGroup, Panel, Modal, Popover |
| Input | No | TextInput, NumberInput, Select, Toggle, … |
| Display | No | Heading, Text, Divider, Alert, Badge, … |
Rules:
-
Layout and Container components MAY contain any component type as children (Layout, Container, Input, or Display), unless further restricted by the specific component (e.g., Tabs children SHOULD be Page components for correct tab rendering).
-
Input and Display components MUST NOT have a
childrenproperty. If present, processors MUST reject the document or ignore thechildrenproperty and emit a warning. -
Spacer MUST NOT have children (it is a Layout leaf).
-
Nesting depth SHOULD NOT exceed 20 levels. Processors MAY reject documents exceeding this limit.
The optional accessibility property on any component object is an
AccessibilityBlock — a flat object that lets authors override or
supplement the ARIA attributes applied to the component's root element.
| Property | Type | Description |
|---|---|---|
role |
string | ARIA role override (e.g., "region", "group", "status"). Replaces any renderer-default role. |
description |
string | Accessible description text. Renderers SHOULD wire this to aria-describedby (not aria-description, which is not a valid ARIA attribute). |
liveRegion |
"off" | "polite" | "assertive" |
Sets aria-live on the root element. Renderers MUST NOT apply role="status" or any live-region semantics unless this property is explicitly set. |
Renderers MUST apply all present AccessibilityBlock properties to the
component's root DOM element. If a property is absent, the renderer's
default behaviour is preserved.
The following table lists component properties that contain
human-readable text addressable by Locale Documents via
$component.<id>.<prop> keys. Only components with an id
property are addressable.
| Component | Localizable Props |
|---|---|
| Page | title, description |
| Heading | text |
| Text | text |
| Alert | text |
| Divider | label |
| Card | title, subtitle |
| Collapsible | title |
| ConditionalGroup | fallback |
| Tabs | tabLabels[N] |
| Accordion | labels[N] |
| SubmitButton | label, pendingLabel |
| DataTable | columns[N].header |
| Panel | title |
| Modal | title, triggerLabel |
| Popover | triggerLabel |
| Badge | text |
| ProgressBar | label |
| Summary | items[N].label |
| Select | placeholder |
| TextInput | placeholder, prefix, suffix |
| NumberInput | placeholder |
| DatePicker | placeholder |
| MoneyInput | placeholder |
Array-valued properties use bracket indexing with numeric indices
(e.g., $component.mainTabs.tabLabels[0]).
Slot binding is the mechanism by which components in the presentation tree
are associated with items in the Formspec Definition. The bind property
on a component object establishes this association.
The bind property is a string that identifies an item in the
target Definition. It accepts two forms:
- Flat key — a single
keymatching a top-level item (e.g.,"projectName"). - Dotted qualified path — a dot-delimited path from a group key
to a nested child key (e.g.,
"applicantInfo.orgName").
The bind value is NOT a JSON Pointer or FEL expression.
// Top-level item — flat key:
{ "component": "TextInput", "bind": "projectName" }
// Nested item — dotted qualified path:
{ "component": "TextInput", "bind": "applicantInfo.orgName" }The bind value MUST be a non-empty string. The value MUST correspond
to an item key (or a dotted path resolving to a nested item) in the
target Definition. If the key does not resolve to any item in the
Definition, the processor MUST emit a warning and SHOULD hide
the component.
The meaning and requirement of bind varies by component category:
| Category | bind |
Behavior |
|---|---|---|
| Input | REQUIRED | The component reads and writes the bound item's value. The renderer MUST propagate the item's required, readOnly, and relevant state to the input control. Validation errors for the bound key MUST be displayed adjacent to this component. |
| Display | OPTIONAL | When present, the component displays the bound item's current value as read-only content. When absent, the component renders its static text prop. |
| Layout | FORBIDDEN | Layout components MUST NOT have a bind property. If present, processors MUST ignore it and emit a warning. |
| Container | FORBIDDEN | Container components MUST NOT have a bind property, with the exceptions of DataTable (§6.14) and Accordion (§6.3), which MAY bind to a repeatable group. |
When an Input component is bound to a field item:
- Label: The renderer MUST display the item's
label. - Hint: The renderer SHOULD display the item's
hintwhen available. - Required indicator: The renderer MUST indicate required state when
the item's Bind
requiredexpression evaluates totrue. - Read-only state: The renderer MUST disable editing when the item's
Bind
readOnlyexpression evaluates totrueor when the item has acalculateBind. - Relevant state: When the item's Bind
relevantexpression evaluates tofalse, the renderer MUST hide the component. This operates independently of the component'swhenproperty (§8.2). - Validation errors: The renderer MUST display validation results (core §5.6) adjacent to the input component that binds the errored key.
At most one editable Input component MAY bind to a given item key. If two or more editable Input components bind to the same key, the processor MUST reject the document or emit a warning and bind only the first occurrence.
Multiple read-only Display components MAY bind to the same key. This is useful for showing a field's value in a summary section while also rendering an input elsewhere.
Example:
// ✓ VALID — one input + one display for same key
{ "component": "NumberInput", "bind": "totalBudget" }
// ... elsewhere in the tree:
{ "component": "Text", "bind": "totalBudget" }
// ✗ INVALID — two inputs for same key
{ "component": "NumberInput", "bind": "totalBudget" }
{ "component": "Slider", "bind": "totalBudget" }Informative note: This constraint prevents conflicting write paths. If a future extension requires multiple input modalities for the same field, it should define explicit synchronization semantics.
When a component binds to a repeatable group item (an item with
type: "group" and minRepeat/maxRepeat in the Definition), the
component acts as a repeat template.
The renderer MUST:
- Render one instance of the component (and its children) for each repeat instance in the data.
- Within each repeat instance, resolve child
bindvalues relative to the repeat context. Child keys are still flat item keys, but they resolve within the current repeat instance. - Provide affordances for adding and removing repeat instances, subject
to
minRepeatandmaxRepeatconstraints from the Definition.
Repeatable group binding is available on DataTable (§6.14), where each repeat instance becomes a table row, and on Accordion (§6.3), where each repeat instance becomes a collapsible section.
Other layout and container components MUST NOT bind to repeatable groups. Processors MUST reject such bindings.
A Component Document is NOT required to bind every item in the Definition.
However, the renderer MUST ensure that all required items (items
whose Bind required evaluates to true) are rendered and editable.
For required items that are NOT bound to any Input component in the tree:
- The renderer MUST render a fallback input for each unbound required item.
- Fallback rendering MUST use Tier 2 theme rules if a Theme Document
is present, or Tier 1
presentationhints otherwise, or renderer defaults as a last resort. - Fallback inputs MUST be appended after the component tree's rendered output, in Definition document order.
- The renderer SHOULD visually distinguish fallback-rendered items (e.g., with a "Additional required fields" heading).
For non-required items that are not bound, the renderer MAY omit them
entirely. If the Definition's relevant expression for an unbound item
evaluates to true and the item is visible, the renderer SHOULD render
it using fallback rules.
Each Input component declares which Definition dataType values it is
compatible with. Binding a component to an item with an incompatible
dataType is a validation error.
dataType |
Compatible Input Components |
|---|---|
string |
TextInput |
number |
NumberInput, Slider, Rating |
integer |
NumberInput, Slider, Rating |
boolean |
Toggle |
date |
DatePicker |
dateTime |
DatePicker |
time |
DatePicker |
choice |
Select, RadioGroup |
multiChoice |
CheckboxGroup, Select |
attachment |
FileUpload, Signature |
Notes:
- NumberInput is compatible with
number,integer, and (via formatting) items that useprefix/suffixfor currency display. - Slider and Rating are Progressive components; their fallback is NumberInput.
- RadioGroup is a Progressive component; its fallback is Select.
- Select on a
multiChoiceitem MUST setmultipletotrueso the control stores an array of values (same shape as CheckboxGroup). Using Select withoutmultipleonmultiChoiceis incompatible with the stored value type. - Signature is a Progressive component; its fallback is FileUpload.
- Display components (Text, Heading, etc.) are compatible with any
dataTypewhen used in read-only mode viabind.
Processors MUST validate bind/dataType compatibility and MUST reject or warn on incompatible bindings.
This section defines the 18 Core components that all conforming processors MUST support. Components are grouped by category: Layout, Input, Display, and Container.
For each component, the specification provides:
- Category and Level classification.
- Whether the component accepts children.
- The bind requirement (Required, Optional, or Forbidden).
- Compatible dataTypes (Input components only).
- A description, props table, rendering requirements, and example.
Category: Layout Level: Core Accepts children: Yes Bind: Forbidden
A top-level page container representing a logical section of a form.
When formPresentation.pageMode is "wizard" or "tabs", Pages define
the navigation steps or tab panels. Pages MAY also be used standalone
within a Stack for single-page sectioned forms.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
title |
string | — | No | Page heading displayed at the top of the section. |
description |
string | — | No | Subtitle or description text rendered below the title. |
- MUST render as a block-level section element (e.g.,
<section>or equivalent). - When
titleis present, MUST render it as a heading element. - When
formPresentation.pageModeis"wizard", the Page MUST be shown/hidden according to the current step navigation state. - MUST render children in array order within the section.
{
"component": "Page",
"title": "Project Information",
"description": "Enter basic details about your project.",
"children": [
{ "component": "TextInput", "bind": "projectName" },
{ "component": "TextInput", "bind": "projectCode" }
]
}Category: Layout Level: Core Accepts children: Yes Bind: Forbidden
A flexbox-style stacking container that arranges its children in a vertical or horizontal sequence. Stack is the most common layout primitive and is typically used as the root component.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
direction |
string | "vertical" |
No | Stack direction. MUST be one of "vertical" or "horizontal". |
gap |
string | number | 0 |
Yes | Spacing between children. String values (e.g., "16px", "$token.spacing.md") or numeric pixel values. |
align |
string | "stretch" |
No | Cross-axis alignment. MUST be one of "start", "center", "end", "stretch". |
wrap |
boolean | false |
No | Whether children wrap to the next line when direction is "horizontal". |
- MUST render as a flex container with the specified direction.
- MUST apply
gapbetween adjacent visible children. - MUST apply
alignto the cross-axis. - When
wrapistrueand direction is horizontal, children MUST wrap to new rows when they exceed container width.
{
"component": "Stack",
"direction": "vertical",
"gap": "$token.spacing.md",
"children": [
{ "component": "TextInput", "bind": "firstName" },
{ "component": "TextInput", "bind": "lastName" }
]
}Category: Layout Level: Core Accepts children: Yes Bind: Forbidden
A multi-column grid layout that distributes children across columns. Children are placed in source order, wrapping to new rows as needed.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
columns |
integer | string | 2 |
Yes | Number of columns (integer) or a CSS grid-template-columns value (string, e.g., "1fr 2fr 1fr"). |
gap |
string | number | 0 |
Yes | Spacing between grid cells. |
rowGap |
string | number | (inherits gap) |
Yes | Vertical spacing between rows, if different from gap. |
- MUST render as a grid container with the specified column count or template.
- MUST distribute children into cells in source order, left-to-right then top-to-bottom (for LTR locales).
- MUST apply gap spacing between cells.
{
"component": "Grid",
"columns": 3,
"gap": "$token.spacing.md",
"children": [
{ "component": "TextInput", "bind": "firstName" },
{ "component": "TextInput", "bind": "middleName" },
{ "component": "TextInput", "bind": "lastName" }
]
}The Wizard component type was removed in favor of
formPresentation.pageMode: "wizard" with a Stack > Page* tree
structure. See Core §4.1.2 for normative page mode processing
requirements. Wizard-style navigation is now a presentation mode
applied to a Stack of Pages, not a distinct component type.
Category: Layout Level: Core Accepts children: No Bind: Forbidden
An empty spacing element that inserts visual space between siblings. Spacer is a leaf component with no children and no binding.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
size |
string | number | "$token.spacing.md" |
Yes | The amount of space. String values (e.g., "24px") or numeric pixel values. |
- MUST render as an empty block element with the specified size as its height (in a vertical context) or width (in a horizontal context).
- MUST NOT render any visible content.
- MUST NOT accept children. If
childrenis present, processors MUST ignore it.
{ "component": "Spacer", "size": "$token.spacing.lg" }Category: Input
Level: Core
Accepts children: No
Bind: Required
Compatible dataTypes: string, number (as text), date (as text), time (as text), dateTime (as text)
A single-line or multi-line text input field. This is the default
input component for string-type fields. When maxLines is greater
than 1, the input renders as a multi-line textarea.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
placeholder |
string | — | No | Placeholder text displayed when the field is empty. |
maxLines |
integer | 1 |
No | Maximum visible lines. 1 = single-line input, >1 = multi-line textarea. MUST be ≥ 1. |
inputMode |
string | "text" |
No | Input mode hint. One of "text", "email", "tel", "url", "search". |
prefix |
string | — | No | Static text rendered before the input (e.g., "https://"). |
suffix |
string | — | No | Static text rendered after the input (e.g., ".com"). |
variant |
string | "plain" |
No | Content-type variant. "plain" accepts unstyled text. "richtext" accepts formatted text with runtime-defined serialization. "markdown" accepts Markdown source — portable, diffable, and degrades gracefully to plain text. "latex" accepts LaTeX source — authoritative for mathematical and scientific notation; degrades to raw source on renderers without a LaTeX renderer. "richtext", "markdown", and "latex" all MUST bind to a string or text field. |
- MUST render as a text input element (
<input type="text">or<textarea>for multi-line) whenvariantis"plain"(default). - When
variantis"richtext", MUST render a content-editable rich-text surface capable of round-tripping bold, italic, and link formatting through the bound field's serialized string value. The on-the-wire serialization is runtime-defined; a registry extension MAY pin a canonical format. - When
variantis"markdown", MUST render a Markdown-aware surface (plain-text editor with preview, or a source-of-truth-Markdown WYSIWYG) and store the raw Markdown source in the bound string field. Markdown is the preferred variant when the document will be diffed, version- controlled, or consumed by non-renderer tooling — the source is plain text and degrades gracefully. - When
variantis"latex", MUST render a LaTeX-aware surface (source editor with an optional rendered preview via a LaTeX engine such as KaTeX or MathJax) and store the raw LaTeX source in the bound string field. LaTeX is the authoritative variant for mathematical, scientific, and typeset-document content; a renderer without a LaTeX engine MUST display the raw source rather than silently discard formatting. - For
"richtext","markdown", and"latex", the bound field'sdataTypeMUST bestringortext— none of these representations is encodable in any other primitive. A non-string bind with any of these variants MUST produce a lint error (E804). - MUST propagate the bound item's
required,readOnly, andrelevantstate. - MUST display validation errors from the bound item.
- MUST apply
inputModeas an input hint for virtual keyboards. - When the bound item has a
maxLengthconstraint, the renderer SHOULD indicate the limit.
{
"component": "TextInput",
"bind": "email",
"placeholder": "you@example.com",
"inputMode": "email"
}{
"component": "TextInput",
"bind": "notes",
"variant": "richtext",
"maxLines": 8
}{
"component": "TextInput",
"bind": "description",
"variant": "markdown",
"maxLines": 12,
"placeholder": "# Heading\n\nWrite in Markdown…"
}{
"component": "TextInput",
"bind": "abstractEquation",
"variant": "latex",
"maxLines": 6,
"placeholder": "\\int_0^\\infty e^{-x^2}\\,dx"
}Category: Input
Level: Core
Accepts children: No
Bind: Required
Compatible dataTypes: integer, number
A numeric input field with optional step controls. Suitable for integers, decimals, and monetary values (when paired with prefix/suffix).
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
placeholder |
string | — | No | Placeholder text displayed when the field is empty. |
step |
number | 1 |
No | Increment/decrement step value. |
min |
number | — | No | Minimum allowed value. |
max |
number | — | No | Maximum allowed value. |
showStepper |
boolean | false |
No | Whether to show increment/decrement buttons. |
locale |
string | — | No | Locale for number formatting (e.g., "en-US"). |
- MUST render as a numeric input element (
<input type="number">or equivalent). - MUST reject non-numeric input at the UI level.
- MUST propagate
required,readOnly, andrelevantfrom the bound item. - MUST display validation errors from the bound item.
- When
minormaxis specified, MUST constrain the stepper controls accordingly.
{
"component": "NumberInput",
"bind": "quantity",
"placeholder": "0",
"min": 1,
"max": 100,
"step": 1,
"showStepper": true
}Category: Input
Level: Core
Accepts children: No
Bind: Required
Compatible dataTypes: date, dateTime, time
A date, datetime, or time picker control. The picker mode is
automatically determined by the bound item's dataType.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
placeholder |
string | — | No | Placeholder text displayed when the field is empty, when the host platform exposes placeholders for date/time controls. |
format |
string | — | No | Display format hint (e.g., "YYYY-MM-DD", "MM/DD/YYYY"). Does not affect stored value (always ISO 8601). |
minDate |
string | — | No | Earliest selectable date (ISO 8601). |
maxDate |
string | — | No | Latest selectable date (ISO 8601). |
showTime |
boolean | false |
No | Whether to include time selection (relevant for dateTime). |
- MUST render an appropriate picker for the bound dataType:
date→ date pickerdateTime→ date + time pickertime→ time picker
- MUST store values in ISO 8601 format regardless of display format.
- MUST propagate
required,readOnly, andrelevantstate. - MUST display validation errors.
- When
minDateormaxDateis specified, MUST disable dates outside the range.
{
"component": "DatePicker",
"bind": "startDate",
"placeholder": "YYYY-MM-DD",
"format": "MM/DD/YYYY",
"minDate": "2025-01-01"
}Category: Input
Level: Core
Accepts children: No
Bind: Required
Compatible dataTypes: choice, multiChoice
A single- or multi-select control for choice lists. Options are read from
the bound item's options array or optionSet reference in the
Definition.
By default (when searchable and multiple are both false or omitted),
processors SHOULD render a native HTML <select> (or platform
equivalent) for a compact dropdown.
When searchable is true and/or multiple is true, processors MUST
render an accessible combobox pattern: a text field (filter and/or
summary), an associated listbox, and keyboard support consistent with
WAI-ARIA combobox/listbox guidance. For multiple, the listbox MUST
allow toggling several options (e.g., checkboxes per row) and MUST store
an array of selected option values in the response data.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
searchable |
boolean | false |
No | Use a combobox with optional type-ahead filtering of option labels. When false and multiple is false, a native <select> is used. |
multiple |
boolean | false |
No | Allow multiple selections; bind to a multiChoice item. Implies a combobox list; combine with searchable for filtering. |
placeholder |
string | "Select…" |
No | Placeholder text when no option is selected (or when the closed combobox shows an empty state). |
clearable |
boolean | false |
No | Whether the user can clear the selection (null for single; empty array for multiple). |
- MUST read options from the bound item's
optionsoroptionSet. - MUST display the option
labelto the user and store the optionvaluein the data (single scalar forchoice; array of values formultiChoicewhenmultipleistrue). - MUST propagate
required,readOnly, andrelevantstate. - MUST display validation errors.
- When
searchableisfalseandmultipleisfalse, MUST render a single-select dropdown (native<select>or equivalent). - When
searchableistrueormultipleistrue, MUST expose a combobox (role="combobox") and listbox (role="listbox") witharia-expanded,aria-controls, andaria-activedescendant(or equivalent) as appropriate; whenmultipleistrue, the listbox MUST setaria-multiselectable="true". - When
searchableistrue, MUST filter visible options by the user's typed query using a case-insensitive substring match against each option'slabel, its storedvalue, and any strings in the definition option's optionalkeywordsarray (for abbreviations and alternate names).
{
"component": "Select",
"bind": "department",
"searchable": true,
"placeholder": "Choose a department"
}Multi-select combobox:
{
"component": "Select",
"bind": "tags",
"multiple": true,
"searchable": true,
"placeholder": "Select tags"
}Category: Input
Level: Core
Accepts children: No
Bind: Required
Compatible dataTypes: multiChoice
A group of checkboxes for multi-select fields. Options are read from
the bound item's options or optionSet.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
columns |
integer | 1 |
No | Number of columns to arrange checkboxes in. |
selectAll |
boolean | false |
No | Whether to display a "Select All" control. |
- MUST render one checkbox per option.
- MUST allow multiple simultaneous selections.
- MUST store the value as an array of selected option values.
- MUST propagate
required,readOnly, andrelevantstate. - MUST display validation errors.
- When
selectAllistrue, MUST provide a master toggle control.
{
"component": "CheckboxGroup",
"bind": "interests",
"columns": 2,
"selectAll": true
}Category: Input
Level: Core
Accepts children: No
Bind: Required
Compatible dataTypes: boolean
A boolean switch/toggle control. Suitable for yes/no, on/off, or true/false fields.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
onLabel |
string | "On" |
No | Label displayed when the toggle is in the true state. |
offLabel |
string | "Off" |
No | Label displayed when the toggle is in the false state. |
- MUST render as a switch/toggle control (not a checkbox).
- MUST store
trueorfalsein the data. - MUST propagate
required,readOnly, andrelevantstate. - MUST display validation errors.
- MUST display the appropriate label (
onLabel/offLabel) for the current state.
{
"component": "Toggle",
"bind": "agreeToTerms",
"onLabel": "I agree",
"offLabel": "I do not agree"
}Category: Input
Level: Core
Accepts children: No
Bind: Required
Compatible dataTypes: attachment
A file upload control for attachment-type fields. Supports single or multiple file selection with optional type and size constraints.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
accept |
string | "*/*" |
No | Accepted MIME types (comma-separated, e.g., "image/*,application/pdf"). |
maxSize |
integer | — | No | Maximum file size in bytes. |
multiple |
boolean | false |
No | Whether multiple files may be uploaded. |
dragDrop |
boolean | true |
No | Whether to display a drag-and-drop zone. |
- MUST render a file selection control.
- MUST filter selectable files by
acceptMIME types when the platform supports it. - MUST validate file size against
maxSizebefore upload. - MUST propagate
required,readOnly, andrelevantstate. - MUST display validation errors.
- When
multipleistrue, MUST allow selection of multiple files. - MUST display the filename(s) of selected files.
{
"component": "FileUpload",
"bind": "supportingDocuments",
"accept": "application/pdf,image/*",
"maxSize": 10485760,
"multiple": true
}Category: Display Level: Core Accepts children: No Bind: Forbidden
A section heading element. Used to structure the visual hierarchy of the form. Heading is purely presentational and does not bind to data.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
level |
integer | — (REQUIRED) | No | Heading level, 1–6. MUST correspond to HTML heading semantics (<h1>–<h6>). |
text |
string | — (REQUIRED) | No | The heading text content. |
- MUST render as a heading element at the specified level.
- MUST render the
textcontent. - MUST NOT accept a
bindproperty. If present, processors MUST ignore it.
{ "component": "Heading", "level": 2, "text": "Budget Details" }Category: Display Level: Core Accepts children: No Bind: Optional
A block of static or data-bound text. When bind is present, displays
the bound item's current value as read-only text. When bind is
absent, displays the static text prop.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
text |
string | "" |
No | Static text content. Ignored when bind is present. |
format |
string | "plain" |
No | Text format. MUST be one of "plain" or "markdown". When "markdown", renderers SHOULD render basic Markdown formatting (bold, italic, links, lists). |
- MUST render as a paragraph or inline text element.
- When
bindis present, MUST display the bound item's formatted value. The renderer SHOULD apply appropriate formatting based on the item'sdataType(e.g., date formatting, number formatting). - When
formatis"markdown", MUST render basic Markdown. Renderers MUST sanitize Markdown output to prevent script injection.
// Static text
{ "component": "Text", "text": "Please review before submitting.", "format": "markdown" }
// Bound text
{ "component": "Text", "bind": "totalBudget" }Category: Display Level: Core Accepts children: No Bind: Forbidden
A horizontal rule used to visually separate sections of the form.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
label |
string | — | No | Optional label text centered on the divider line. |
- MUST render as a horizontal rule (
<hr>or equivalent). - When
labelis present, MUST display the text centered on or adjacent to the rule. - MUST NOT accept a
bindproperty.
{ "component": "Divider", "label": "Section Break" }Category: Container Level: Core Accepts children: Yes Bind: Forbidden
A bordered surface that visually groups related content. Cards provide a visual boundary with optional title and subtitle.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
title |
string | — | No | Card header title. |
subtitle |
string | — | No | Card header subtitle, rendered below the title. |
elevation |
string | "low" |
Yes | Shadow depth. SHOULD map to a token (e.g., "$token.elevation.low"). |
- MUST render as a visually distinct surface with a border or shadow.
- When
titleis present, MUST render a card header. - MUST render children in array order within the card body.
{
"component": "Card",
"title": "Contact Information",
"children": [
{ "component": "TextInput", "bind": "email" },
{ "component": "TextInput", "bind": "phone" }
]
}Category: Container Level: Core Accepts children: Yes Bind: Forbidden
An expandable/collapsible section. The user can toggle visibility of the children. Useful for optional sections or advanced options.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
title |
string | — (REQUIRED) | No | The collapsible section header. MUST be visible regardless of open/closed state. |
defaultOpen |
boolean | false |
No | Whether the section is initially expanded. |
- MUST render a clickable header that toggles child visibility.
- MUST display the
titlein the header. - When collapsed, children MUST be hidden but MUST remain in the DOM/component tree (their bound data is preserved).
- Collapsed children's
relevantandwhenstate MUST still be evaluated. - MUST apply appropriate ARIA attributes (
aria-expanded, etc.).
{
"component": "Collapsible",
"title": "Advanced Options",
"defaultOpen": false,
"children": [
{ "component": "Toggle", "bind": "enableNotifications" },
{ "component": "Select", "bind": "notificationFrequency" }
]
}Category: Container Level: Core Accepts children: Yes Bind: Forbidden
A container whose visibility is controlled by a required when
expression. Unlike the optional when property available on all
components (§8), ConditionalGroup makes the condition its primary
purpose — it exists solely to conditionally show/hide a group of
children.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
when |
string (FEL) | — (REQUIRED) | No | FEL boolean expression. When false or null, the group and all children are hidden. REQUIRED on ConditionalGroup (unlike optional when on other components). |
fallback |
string | — | No | Optional text to display when the condition is false. |
- MUST evaluate the
whenexpression against the data tree. - When the expression evaluates to
true, MUST render all children. - When the expression evaluates to
falseornull, MUST hide all children. Iffallbacktext is present, MUST display it in place of the hidden children. - Data-bound children within a hidden ConditionalGroup retain their
data values (unlike Bind
relevant, which may clear data). - A ConditionalGroup without a
whenexpression is invalid. Processors MUST reject such documents.
{
"component": "ConditionalGroup",
"when": "$hasEmployer = true",
"fallback": "Employer details are not required for this application type.",
"children": [
{ "component": "TextInput", "bind": "employerName" },
{ "component": "TextInput", "bind": "employerAddress" }
]
}Category: Display Level: Core Accepts children: No Bind: Forbidden
A button that triggers the renderer's submit flow. When clicked, the
renderer collects the current form response, generates a validation
report in the configured mode, and either dispatches a
formspec-submit CustomEvent (when emitEvent is true) or calls
the host renderer's submit API directly.
While a submission is pending (the shared submit-pending state is
true), the button SHOULD display pendingLabel (when provided) in
place of label, and — unless disableWhenPending is false — MUST
be rendered in a disabled/inert state to prevent duplicate submissions.
SubmitButton has no bind relationship. It interacts with the form
as a whole, not with any individual item.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
label |
string | "Submit" |
No | Button label text. |
mode |
string | "submit" |
No | Validation mode used when clicked. MUST be one of "continuous" or "submit". Controls which validation pass produces the report emitted with the event. |
emitEvent |
boolean | false |
No | When true, clicking the button dispatches a formspec-submit CustomEvent whose detail carries the current response and validation report. |
pendingLabel |
string | — | No | Label text shown while the shared submit-pending state is true. Falls back to label when absent. |
disableWhenPending |
boolean | true |
No | Whether the button is rendered in a disabled/inert state while the shared submit-pending state is true. |
- MUST render as a native button element or equivalent accessible interactive control.
- MUST NOT accept a
bindproperty. - When clicked and
emitEventistrue, MUST dispatch aformspec-submitCustomEvent on the host element. - When the shared submit-pending state is
true:- If
disableWhenPendingistrue, MUST render the button as disabled/inert. - MUST display
pendingLabelwhen present.
- If
{ "component": "SubmitButton", "label": "Submit Application", "mode": "submit", "emitEvent": true, "pendingLabel": "Submitting…" }This section defines the 17 Progressive components. A Complete Conformant processor MUST support all 17. A Core Conformant processor MUST substitute the specified Core fallback for each Progressive component and SHOULD emit an informative warning.
Each Progressive component entry includes a Fallback line identifying the Core component that replaces it in Core-level processors. §6.18 provides a consolidated fallback table.
Category: Layout Level: Progressive Accepts children: Yes Bind: Forbidden Fallback: Grid
An explicit multi-column layout where each child occupies a column
whose width is specified by the widths array. Unlike Grid, which
auto-distributes children into equal cells, Columns gives precise
control over per-column sizing.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
widths |
array of strings | equal widths | No | Per-child column widths as CSS values (e.g., ["1fr", "2fr", "1fr"] or ["200px", "auto"]). Array length SHOULD match the number of children. |
gap |
string | number | 0 |
Yes | Spacing between columns. |
- MUST render children side-by-side in the specified widths.
- When
widthslength differs from children count, MUST distribute remaining children into equal-width columns.
Core processors MUST replace Columns with a Grid whose columns
prop equals the number of children. The gap prop is preserved.
{
"component": "Columns",
"widths": ["2fr", "1fr"],
"gap": "$token.spacing.md",
"children": [
{ "component": "TextInput", "bind": "address" },
{ "component": "TextInput", "bind": "zipCode" }
]
}Category: Layout Level: Progressive Accepts children: Yes Bind: Forbidden Fallback: Stack (each child preceded by a Heading)
A tabbed navigation container. Each direct child represents the
content of one tab. Tab labels are derived from child Page title
props or from the tabLabels array.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
position |
string | "top" |
No | Tab bar position. MUST be one of "top", "bottom", "left", or "right". |
tabLabels |
array of strings | — | No | Explicit tab labels. When absent, the renderer reads title from each child (children SHOULD be Page components). |
defaultTab |
integer | 0 |
No | Zero-based index of the initially active tab. |
- MUST render a tab bar with one tab per direct child.
- MUST show exactly one child's content at a time.
- MUST allow the user to switch tabs by clicking tab labels.
- All children remain mounted; switching tabs changes visibility, not lifecycle. Bound data is preserved.
Core processors MUST replace Tabs with a Stack (direction
"vertical"). Each child is preceded by a Heading (level 3)
whose text is the corresponding tab label. All children are rendered
visibly in sequence.
{
"component": "Tabs",
"tabLabels": ["Personal", "Employment", "Review"],
"children": [
{ "component": "Stack", "children": [
{ "component": "TextInput", "bind": "firstName" },
{ "component": "TextInput", "bind": "lastName" }
]},
{ "component": "Stack", "children": [
{ "component": "TextInput", "bind": "employer" }
]},
{ "component": "Stack", "children": [
{ "component": "Text", "text": "Please review your information." }
]}
]
}Category: Layout Level: Progressive Accepts children: Yes Bind: Optional (repeatable group key) Fallback: Stack with Collapsible children
A vertical list of collapsible sections where, by default, only one
section is expanded at a time. Each child SHOULD be a component with
a title prop (e.g., Page, Card, Collapsible) to serve as the
section header.
When bind is provided, it MUST reference a repeatable group item.
Each repeat instance becomes one accordion section. Child bind
values resolve relative to the repeat context.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
allowMultiple |
boolean | false |
No | Whether multiple sections may be expanded simultaneously. When false, expanding one section collapses the others. |
defaultOpen |
integer | 0 |
No | Zero-based index of the initially expanded section. |
labels |
string[] | — | No | Section header labels. labels[i] is the summary text for children[i]. Falls back to "Section {i+1}" when absent. |
- MUST render each child as a collapsible panel with a clickable header.
- When
allowMultipleisfalse, MUST enforce mutual exclusion (only one open at a time). - MUST apply appropriate ARIA roles (
role="region",aria-expanded, etc.).
Core processors MUST replace Accordion with a Stack where each
child is wrapped in a Collapsible. The first child's Collapsible
has defaultOpen: true; the rest have defaultOpen: false.
{
"component": "Accordion",
"allowMultiple": false,
"children": [
{ "component": "Page", "title": "Section A", "children": [
{ "component": "TextInput", "bind": "fieldA" }
]},
{ "component": "Page", "title": "Section B", "children": [
{ "component": "TextInput", "bind": "fieldB" }
]}
]
}Category: Input
Level: Progressive
Accepts children: No
Bind: Required
Compatible dataTypes: choice
Fallback: Select
A group of radio buttons for single-select choice fields. All options are visible simultaneously, making RadioGroup suitable for short option lists (typically ≤ 7 items).
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
columns |
integer | 1 |
No | Number of columns to arrange radio buttons in. |
orientation |
string | "vertical" |
No | Layout direction. MUST be "vertical" or "horizontal". |
- MUST render one radio button per option from the bound item's
optionsoroptionSet. - MUST enforce single selection (selecting one deselects others).
- MUST propagate
required,readOnly, andrelevantstate. - MUST display validation errors.
Core processors MUST replace RadioGroup with Select. The
searchable prop defaults to false. The columns prop is
discarded.
{
"component": "RadioGroup",
"bind": "priority",
"columns": 3,
"orientation": "horizontal"
}Category: Input
Level: Progressive
Accepts children: No
Bind: Required
Compatible dataTypes: number, integer
Fallback: NumberInput
A currency-aware numeric input that displays a currency symbol and formatted number. Stores the raw numeric value without currency formatting.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
placeholder |
string | — | No | Placeholder text displayed in the amount input when the field is empty. |
currency |
string | "USD" |
No | ISO 4217 currency code (e.g., "USD", "EUR", "GBP"). |
showCurrency |
boolean | true |
No | Whether to display the currency symbol. |
locale |
string | — | No | Locale for number/currency formatting (e.g., "en-US"). |
- MUST render a numeric input with the currency symbol.
- MUST format the displayed value according to the locale's currency conventions.
- MUST store the raw numeric value (without formatting characters) in the data.
- MUST propagate
required,readOnly, andrelevantstate. - MUST display validation errors.
Core processors MUST replace MoneyInput with NumberInput. The
currency symbol SHOULD be rendered as a prefix label adjacent to
the input if the bound item has a prefix presentation hint.
{
"component": "MoneyInput",
"bind": "totalBudget",
"currency": "USD",
"showCurrency": true,
"placeholder": "0.00"
}Category: Input
Level: Progressive
Accepts children: No
Bind: Required
Compatible dataTypes: integer, number
Fallback: NumberInput
A range slider control for selecting a numeric value within a continuous range.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
min |
number | 0 |
No | Minimum value. |
max |
number | 100 |
No | Maximum value. |
step |
number | 1 |
No | Step increment. |
showValue |
boolean | true |
No | Whether to display the current numeric value adjacent to the slider. |
showTicks |
boolean | false |
No | Whether to display tick marks at step intervals. |
- MUST render as a range slider control.
- MUST constrain the value to the
min–maxrange. - MUST snap to
stepincrements. - When
showValueistrue, MUST display the current value. - MUST propagate
required,readOnly, andrelevantstate. - MUST display validation errors.
Core processors MUST replace Slider with NumberInput. The min,
max, and step props are preserved on the NumberInput.
{
"component": "Slider",
"bind": "satisfaction",
"min": 1,
"max": 10,
"step": 1,
"showValue": true
}Category: Input
Level: Progressive
Accepts children: No
Bind: Required
Compatible dataTypes: integer
Fallback: NumberInput
A star (or icon) rating control for selecting an integer value within a small range (typically 1–5 or 1–10).
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
max |
integer | 5 |
No | Maximum rating value (number of stars/icons). |
icon |
string | "star" |
No | Icon type. Well-known values: "star", "heart", "circle". Renderers MAY support additional icons. |
allowHalf |
boolean | false |
No | Whether half-star values are allowed (stored as decimal, e.g., 3.5). |
- MUST render
maxicon elements. - MUST allow the user to select a rating by clicking/tapping.
- MUST store the selected integer (or half-integer if
allowHalf) in the data. - MUST propagate
required,readOnly, andrelevantstate. - MUST display validation errors.
Core processors MUST replace Rating with NumberInput with
min: 1, max preserved, and step: 1.
{
"component": "Rating",
"bind": "serviceRating",
"max": 5,
"icon": "star"
}Category: Input
Level: Progressive
Accepts children: No
Bind: Required
Compatible dataTypes: attachment
Fallback: FileUpload
A signature capture pad that records a drawn signature as an image attachment.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
strokeColor |
string | "#000000" |
Yes | Stroke color for the signature pen. |
height |
integer | 150 |
No | Height of the signature pad in pixels. |
penWidth |
number | 2 |
No | Stroke width in pixels. |
clearable |
boolean | true |
No | Whether to show a clear/reset control. |
- MUST render a drawable canvas area.
- MUST capture the drawn signature and store it as an attachment (image data URL or uploaded file reference).
- MUST provide a "Clear" control to reset the signature.
- MUST propagate
required,readOnly, andrelevantstate. - MUST display validation errors.
Core processors MUST replace Signature with FileUpload with
accept: "image/*".
{
"component": "Signature",
"bind": "approverSignature",
"strokeColor": "#000",
"height": 200
}Category: Display Level: Progressive Accepts children: No Bind: Forbidden Fallback: Text (with severity prefix)
A status message block used for informational banners, warnings, error summaries, or success messages.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
severity |
string | — (REQUIRED) | No | Alert severity level. MUST be one of "info", "success", "warning", "error". |
text |
string | — (REQUIRED) | No | Alert message text. |
dismissible |
boolean | false |
No | Whether the user can dismiss the alert. |
- MUST render with visual styling appropriate to the severity (color, icon).
- MUST use an appropriate ARIA role (
role="alert"for error/warning,role="status"for info/success). - MUST display the
textcontent.
Core processors MUST replace Alert with Text. The text prop
is prefixed with the severity in brackets: e.g., "[Warning] "
- original text.
{
"component": "Alert",
"severity": "warning",
"text": "Budget exceeds department limit. Approval required."
}Category: Display Level: Progressive Accepts children: No Bind: Forbidden Fallback: Text
A small label badge for status indicators, counts, or tags.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
text |
string | — (REQUIRED) | No | Badge label text. |
variant |
string | "default" |
No | Visual variant. Well-known values: "default", "primary", "success", "warning", "error". |
- MUST render as a compact inline label element.
- MUST apply visual styling appropriate to the
variant.
Core processors MUST replace Badge with Text using the
same text prop.
{ "component": "Badge", "text": "Draft", "variant": "warning" }Category: Display Level: Progressive Accepts children: No Bind: Optional Fallback: Text (showing "X / Y")
A visual progress indicator. When bound, reads the current value
from the data. When unbound, uses the static value prop.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
value |
number | 0 |
No | Current progress value. Ignored when bind is present. |
max |
number | 100 |
No | Maximum value (100% completion). |
label |
string | — | No | Accessible label for the progress bar. |
showPercent |
boolean | true |
No | Whether to display the percentage text. |
- MUST render as a progress bar element (
<progress>or equivalent). - MUST compute the fill percentage as
value / max * 100. - MUST apply
aria-valuenow,aria-valuemin, andaria-valuemax.
Core processors MUST replace ProgressBar with Text displaying
the progress as text, e.g., "75 / 100 (75%)".
{
"component": "ProgressBar",
"bind": "completionScore",
"max": 100,
"label": "Form completion"
}Category: Display Level: Progressive Accepts children: No Bind: Forbidden Fallback: Stack of Text components
A key-value summary display that shows multiple field labels and their current values in a structured list. Useful for review pages.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
items |
array | — (REQUIRED) | No | Array of summary items. Each element is an object with label (string, REQUIRED), bind (string, REQUIRED — item key), and optional optionSet (string). |
Each item object in the items array supports:
| Item Field | Type | Required | Description |
|---|---|---|---|
label |
string | Yes | Display label shown next to the value. |
bind |
string | Yes | Path to the field whose value to display. |
optionSet |
string | No | Name of an option set defined in the form definition. When present, the raw bound value is resolved to its display label via the named option set. Use for choice and multiChoice fields. |
- MUST render as a definition list, table, or equivalent key-value layout.
- For each entry in
items, MUST display thelabeland the current value of the bound item. - Values MUST be formatted according to the item's
dataType. - When
optionSetis set on an item, renderers MUST look up the bound value in the named option set and display the matchinglabel. If no match is found, the raw value SHOULD be displayed.
Core processors MUST replace Summary with a Stack containing
one Text component per item, with text set to
"<label>: <value>".
{
"component": "Summary",
"items": [
{ "label": "Project Name", "bind": "projectName" },
{ "label": "Total Budget", "bind": "totalBudget" },
{ "label": "Organization Type", "bind": "orgType", "optionSet": "orgTypes" }
]
}Category: Display Level: Progressive Accepts children: No Bind: Forbidden Fallback: Alert (severity + message rows shown as warning/error alerts)
A validation message panel that surfaces the current form validation
state. Can operate in "live" mode (reading continuous engine
state) or "submit" mode (reading the latest formspec-submit
event detail). Optionally renders jump links that invoke
focusField(path) on affected input fields.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
source |
string | "live" |
No | Validation source. "live" reads continuous engine state; "submit" reads the latest formspec-submit event detail. |
mode |
string | "continuous" |
No | Validation mode used when source is "live". MUST be one of "continuous" or "submit". |
showFieldErrors |
boolean | false |
No | Whether to include bind-level field errors in addition to shape-level findings. |
jumpLinks |
boolean | false |
No | Whether to render clickable links or buttons that call focusField(path) for jumpable targets. |
dedupe |
boolean | true |
No | Whether duplicate messages (same severity, path, and message) are collapsed into a single row. |
- MUST render as a list or panel of validation messages.
- MUST display each finding's severity (error, warning, info) and message text.
- When
jumpLinksistrueand the finding has apath, MUST render a clickable control that callsfocusField(path). - When
dedupeistrue, MUST collapse duplicate findings before rendering. - When no findings are present, the component SHOULD render nothing (empty state) or a brief "No issues" indicator.
Core processors MUST replace ValidationSummary with one or more
Alert components — one per validation finding, using the
finding's severity as the Alert variant.
{ "component": "ValidationSummary", "source": "submit", "jumpLinks": true, "showFieldErrors": true }Category: Display Level: Progressive Accepts children: No Bind: Optional (binds to a repeatable group) Fallback: Stack of bound items
A tabular display of repeatable group data. Each repeat instance
becomes a row; each column displays a field within the repeat.
DataTable is one of the few non-Layout/Container components that
MAY use bind to reference a repeatable group.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
columns |
array | — (REQUIRED) | No | Array of column definitions. Each element is an object with header (string, REQUIRED) and bind (string, REQUIRED — item key within the repeat group). |
showRowNumbers |
boolean | false |
No | Whether to display row numbers. |
allowAdd |
boolean | true |
No | Whether to show an "Add row" control. |
allowRemove |
boolean | true |
No | Whether to show per-row "Remove" controls. |
- MUST render as an HTML table or equivalent tabular layout.
- MUST create one row per repeat instance.
- MUST render one cell per column definition, displaying the value of the bound field within that repeat instance.
- When
allowAddistrue, MUST provide an "Add" affordance, subject tomaxRepeatconstraints. - When
allowRemoveistrue, MUST provide per-row "Remove" affordances, subject tominRepeatconstraints.
Core processors MUST replace DataTable with a Stack that repeats a Card for each repeat instance. Within each Card, bound fields are rendered as TextInput or appropriate Core components.
{
"component": "DataTable",
"bind": "lineItems",
"columns": [
{ "header": "Description", "bind": "description" },
{ "header": "Amount", "bind": "amount" },
{ "header": "Category", "bind": "category" }
]
}Category: Container Level: Progressive Accepts children: Yes Bind: Forbidden Fallback: Card
A side panel used for supplementary content, help text, or contextual actions. Panels may be positioned alongside the main content.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
position |
string | "left" |
No | Panel position. MUST be one of "left" or "right". |
title |
string | — | No | Panel header title. |
width |
string | "300px" |
Yes | Panel width. |
- MUST render the panel alongside (not within) the main content flow,
positioned according to the
positionproperty. - MUST render children within the panel body.
Core processors MUST replace Panel with Card. The title prop
is preserved. The position and width props are discarded.
{
"component": "Panel",
"position": "left",
"title": "Help",
"width": "280px",
"children": [
{ "component": "Text", "text": "Need help? Contact support." }
]
}Category: Container Level: Progressive Accepts children: Yes Bind: Forbidden Fallback: Collapsible
A dialog overlay that displays content in a modal window above the main form. Modals require explicit user action to open and close.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
title |
string | — (REQUIRED) | No | Modal dialog title. |
size |
string | "md" |
No | Modal size. MUST be one of "sm", "md", "lg", "xl", or "full". |
trigger |
string | "button" |
No | How the modal is triggered. MUST be "button" (a dedicated open button) or "auto" (opens automatically based on when). |
triggerLabel |
string | "Open" |
No | Label for the trigger button when trigger is "button". |
closable |
boolean | true |
No | Whether the modal can be dismissed by the user. |
- MUST render as a modal dialog with backdrop overlay.
- MUST trap focus within the modal while open.
- MUST provide a close affordance (button, Escape key) when
closableistrue. - MUST apply
role="dialog"andaria-modal="true". - Content within the modal MUST be interactive (input components receive focus and accept input).
Core processors MUST replace Modal with Collapsible. The title
prop is preserved. The modal's content is rendered as the
collapsible body, initially collapsed (defaultOpen: false).
{
"component": "Modal",
"title": "Terms and Conditions",
"trigger": "button",
"triggerLabel": "View Terms",
"children": [
{ "component": "Text", "text": "By submitting this form, you agree to...", "format": "markdown" }
]
}Category: Container Level: Progressive Accepts children: Yes Bind: Forbidden Fallback: Collapsible
A lightweight anchored overlay that shows contextual content when the trigger is activated.
| Prop | Type | Default | Token-able | Description |
|---|---|---|---|---|
triggerBind |
string | — | No | Optional bind key whose current value is shown as the trigger label. |
triggerLabel |
string | "Open" |
No | Fallback trigger label when triggerBind has no value. |
placement |
string | "bottom" |
No | Preferred popover placement. MUST be one of "top", "right", "bottom", or "left". |
- MUST render a trigger control and a content surface.
- MUST render
childreninside the content surface. - SHOULD use native popover behavior when available.
- MUST provide a usable toggle fallback when native popover behavior is unavailable.
Core processors MUST replace Popover with Collapsible. The
triggerLabel value SHOULD map to Collapsible title. The placement
property is discarded.
{
"component": "Popover",
"triggerBind": "projectName",
"triggerLabel": "Show details",
"placement": "right",
"children": [
{ "component": "Text", "text": "Additional context for this field." }
]
}The following table defines the complete set of Progressive → Core fallback substitutions. A Core Conformant processor MUST apply these fallbacks when it encounters a Progressive component.
| Progressive Component | Core Fallback | Notes |
|---|---|---|
| Columns | Grid | columns set to child count; gap preserved. |
| Tabs | Stack + Heading | Each child preceded by a Heading (level 3) with the tab label. |
| Accordion | Stack + Collapsible | Each child wrapped in Collapsible; first defaults open. |
| RadioGroup | Select | columns discarded. |
| MoneyInput | NumberInput | Currency symbol rendered as prefix if available. |
| Slider | NumberInput | min, max, step preserved. |
| Rating | NumberInput | min: 1, max preserved, step: 1. |
| Signature | FileUpload | accept set to "image/*". |
| Alert | Text | Text prefixed with severity in brackets. |
| Badge | Text | Same text prop. |
| ProgressBar | Text | Text shows "<value> / <max> (<percent>%)". |
| Summary | Stack of Text | One Text per item: "<label>: <value>". |
| ValidationSummary | Alert | One Alert per finding; severity preserved as variant. |
| DataTable | Stack of Card | One Card per repeat instance with child inputs. |
| Panel | Card | title preserved; position/width discarded. |
| Modal | Collapsible | title preserved; defaultOpen: false. |
| Popover | Collapsible | triggerLabel mapped to title; placement discarded. |
Fallback substitution MUST preserve:
- All child components (recursively processed).
- The
whenproperty (transferred to the fallback component). - The
responsiveproperty (transferred if applicable props exist on the fallback). - The
styleproperty. - The
bindproperty (when the fallback supports it).
Fallback substitution MUST discard props that have no equivalent on the Core fallback component. Processors SHOULD emit a warning listing discarded props.
The custom component mechanism allows authors to define reusable component subtrees with parameterized interpolation. Custom components promote consistency and reduce duplication in large component trees.
The top-level components property is an object whose keys are custom
component names and whose values are component template objects.
Each template object has the following properties:
| Property | Type | Cardinality | Description |
|---|---|---|---|
params |
array of strings | 0..1 (OPTIONAL) | Parameter names accepted by this template. Each name MUST match [a-zA-Z][a-zA-Z0-9_]*. |
tree |
object | 1..1 (REQUIRED) | The component subtree that is instantiated when this custom component is used. |
Custom component names MUST match [A-Z][a-zA-Z0-9]* (PascalCase,
starting with uppercase). Names MUST NOT collide with built-in
component names (§5, §6). Names beginning with x- are reserved for
vendor extensions (§13.3).
Example registry:
{
"components": {
"LabeledField": {
"params": ["field", "label"],
"tree": {
"component": "Stack",
"gap": "$token.spacing.sm",
"children": [
{ "component": "Heading", "level": 4, "text": "{label}" },
{ "component": "TextInput", "bind": "{field}" }
]
}
},
"AddressBlock": {
"params": ["prefix"],
"tree": {
"component": "Card",
"title": "Address",
"children": [
{ "component": "TextInput", "bind": "{prefix}Street" },
{ "component": "TextInput", "bind": "{prefix}City" },
{ "component": "TextInput", "bind": "{prefix}State" },
{ "component": "TextInput", "bind": "{prefix}Zip" }
]
}
}
}
}Parameter interpolation uses {paramName} syntax within string-valued
props. The following ABNF grammar defines the interpolation syntax:
interpolated-string = *( literal-segment / param-ref / escaped-brace )
literal-segment = 1*( %x00-7A / %x7C-7C / %x7E-10FFFF ) ; any char except { }
param-ref = "{" param-name "}"
param-name = ALPHA *( ALPHA / DIGIT / "_" )
escaped-brace = "{{" / "}}"Rules:
{paramName}is replaced with the corresponding parameter value from the instantiation'sparamsobject.{{produces a literal{in the output.}}produces a literal}in the output.- Nesting is NOT allowed:
{outer_{inner}}is invalid. - An unrecognized
{name}(wherenameis not in the template'sparamsarray) MUST cause a validation error.
Interpolation is permitted in the following prop types ONLY:
| Allowed | Examples |
|---|---|
bind |
"bind": "{prefix}Street" |
when |
"when": "${field} != null" |
text (on Text, Heading, Alert, Badge) |
"text": "Address for {label}" |
title (on Page, Card, Collapsible, etc.) |
"title": "{sectionTitle}" |
placeholder |
"placeholder": "Enter {label}" |
label (on Divider) |
"label": "{section}" |
fallback (on ConditionalGroup) |
"fallback": "No {item} available" |
Interpolation is FORBIDDEN in:
| Forbidden | Reason |
|---|---|
component |
Component type switching creates ambiguous trees. |
$token.* references |
Token resolution is a separate phase. |
Numeric props (min, max, step, columns, etc.) |
Type safety. |
Boolean props (searchable, showProgress, etc.) |
Type safety. |
style keys or values |
Style resolution is a separate phase. |
A custom component is instantiated by using its registry name as the
component value and providing parameter values in a params object:
{
"component": "AddressBlock",
"params": { "prefix": "home" }
}The processor MUST:
- Look up the component name in the
componentsregistry. - Verify that all declared params have corresponding values in the
instantiation's
paramsobject. Missing params MUST cause a validation error. - Deep-clone the template's
tree. - Replace all
{paramName}occurrences in allowed string props with the corresponding values fromparams. - Insert the resolved subtree in place of the custom component reference.
Extra params (keys in the instantiation's params that are not
declared in the template's params array) MUST be ignored. Processors
SHOULD emit a warning.
The instantiation MAY also include when, style, and responsive
props. These are applied to the root of the resolved subtree
(merged on top of whatever the template already defines).
Custom component templates MUST NOT reference themselves, directly or indirectly. A cycle occurs when template A's tree instantiates template B, and template B's tree instantiates template A (or any longer chain that forms a loop).
Processors MUST perform static cycle detection at validation time by building a directed graph of template references and checking for cycles. Cycle detection MUST be performed regardless of parameter values — the analysis is structural, not data-dependent.
Documents containing recursive custom components MUST be rejected.
Custom component nesting (template A instantiates template B which instantiates template C) SHOULD NOT exceed 3 levels of custom component expansion.
The total tree depth (including both built-in and expanded custom components) SHOULD NOT exceed 20 levels.
Processors MAY enforce stricter limits. Processors MUST NOT enforce limits lower than 3 levels of custom nesting or 10 levels of total tree depth.
The when property enables components to be conditionally shown or
hidden based on the current data state, without affecting the data
model.
The when property is a FEL boolean expression (fel-grammar.md).
When present on a component:
- The processor MUST evaluate the expression against the current data tree.
- If the expression evaluates to
true, the component (and all its children) MUST be rendered. - If the expression evaluates to
false,null, or any non-boolean value, the component (and all its children) MUST be hidden.
The when property is OPTIONAL on all components except
ConditionalGroup (§5.18), where it is REQUIRED.
Multiple when conditions do NOT chain — each component has at most
one when expression. To express compound conditions, use FEL
logical operators within the expression:
{ "component": "TextInput", "bind": "spouseName",
"when": "$maritalStatus = 'married' and $age >= 18" }The when property and the Definition Bind's relevant expression
serve different purposes:
| Aspect | when (Component) |
relevant (Bind) |
|---|---|---|
| Scope | Visual presentation only | Data model inclusion |
| Data effect | None — hidden component's bound data is preserved | Non-relevant data MAY be removed from the Instance (per nonRelevantBehavior) |
| Defined in | Component Document (Tier 3) | Definition (Tier 1, Binds) |
| Evaluation | FEL against data tree | FEL against data tree |
| Override | Cannot override relevant |
Cannot be overridden by when |
When BOTH when and relevant apply to the same bound item:
- If
relevantisfalse, the component is hidden regardless ofwhen. The Bindrelevanttakes precedence. - If
relevantistrueandwhenisfalse, the component is hidden but the data remains in the Instance. - If both are
true, the component is visible.
FEL expressions in when ALWAYS resolve against the data tree
(the Formspec Instance):
$fieldKeyresolves to the current value of the field with that key.@indexresolves to the 1-based repeat index when inside a repeatable group context.@countresolves to the total number of repeat instances when inside a repeatable group context.- Standard FEL functions and operators are available.
FEL expressions MUST NOT reference component props, component state, or presentation-layer values. The evaluation context is strictly the data model.
When a when expression is malformed (syntax error, unresolved
function, type error):
- The processor MUST hide the component (treat as
false). - The processor MUST emit a warning identifying the component and the expression error.
- The processor MUST NOT halt form rendering due to a
whenevaluation error.
When a when expression references an item key that does not exist
in the Definition:
- The missing reference evaluates to
nullper FEL semantics. - The component is hidden (null → falsy).
- The processor SHOULD emit a warning.
The responsive system allows component props to vary by viewport width, enabling adaptive layouts from a single Component Document.
Breakpoints are declared in the top-level breakpoints object. Each
key is a breakpoint name; each value is the minimum viewport width in
pixels at which that breakpoint activates.
{
"breakpoints": {
"sm": 576,
"md": 768,
"lg": 1024,
"xl": 1280
}
}Breakpoint names MUST be non-empty strings. Values MUST be non-negative integers. The same breakpoint format is used in the Theme Specification (theme-spec §6.4).
When a Component Document and a Theme Document both declare
breakpoints for the same Definition, the Component Document's
breakpoints take precedence.
The responsive property on a component object is a JSON object
whose keys are breakpoint names and whose values are prop override
objects:
{
"component": "Grid",
"columns": 3,
"gap": "$token.spacing.md",
"responsive": {
"sm": { "columns": 1, "gap": "$token.spacing.sm" },
"md": { "columns": 2 }
}
}Override objects contain component-specific props only (not base props). The following properties MUST NOT appear in responsive overrides:
component— type switching is forbidden (§9.4).bind— data binding is viewport-independent.when— conditions are viewport-independent.children— tree structure is viewport-independent.responsive— recursive responsive is forbidden.
The style property MAY appear in responsive overrides.
Responsive overrides follow a mobile-first cascade:
- Base props apply to all viewport widths (including the smallest).
- At each breakpoint (in ascending min-width order), the corresponding override object is shallow-merged on top of the base props.
- Each override replaces individual props; it does not deep-merge
nested objects. A
styleoverride replaces the entirestyleobject for that breakpoint.
Resolution algorithm:
function resolveProps(component, viewportWidth, breakpoints):
resolved = copy(component.baseProps)
// Sort breakpoints by minWidth ascending
sorted = sortByValue(breakpoints)
for each (name, minWidth) in sorted:
if viewportWidth >= minWidth:
if component.responsive[name] exists:
shallowMerge(resolved, component.responsive[name])
return resolved
Example: with breakpoints {"sm": 576, "md": 768} and the Grid
example above, at viewport width 700px:
- Base:
columns: 3, gap: "$token.spacing.md" - After
sm(576 ≤ 700):columns: 1, gap: "$token.spacing.sm" mddoes not apply (768 > 700)- Result:
columns: 1, gap: "$token.spacing.sm"
Responsive overrides MUST NOT alter the structural identity of a component:
- The
componenttype MUST NOT change per breakpoint. - The
childrenarray MUST NOT change per breakpoint (no adding, removing, or reordering children). - The
bindproperty MUST NOT change per breakpoint.
These constraints ensure that the component tree is structurally stable across all viewport widths. Only presentational props (layout, spacing, visibility hints) may vary.
Component Documents support design tokens for visual consistency. Tokens defined in a Component Document follow the same format as Tokens in the Theme Specification (theme-spec §3).
The tokens object is a flat key-value map. Keys are dot-delimited
names; values are strings or numbers.
{
"tokens": {
"color.primary": "#0057B7",
"color.error": "#D32F2F",
"spacing.sm": "8px",
"spacing.md": "16px",
"spacing.lg": "24px",
"border.radius": "6px"
}
}Token keys MUST be non-empty strings. Token values MUST be strings or numbers. Tokens MUST NOT contain nested objects, arrays, booleans, or null.
The token format is identical to theme-spec §3.1. The RECOMMENDED
category prefixes (color., spacing., typography., border.,
elevation., x-) from theme-spec §3.2 apply here as well.
Tokens are referenced in style objects and token-able props using
the $token. prefix:
$token.<key>
Examples:
"gap": "$token.spacing.md"resolves to"16px"."style": { "borderRadius": "$token.border.radius" }resolves to"6px".
The reference syntax is identical to theme-spec §3.3. Token
references MUST NOT be recursive (a token value MUST NOT itself
contain a $token. reference).
When both a Component Document and a Theme Document define tokens for the same Definition, the following cascade applies:
| Priority | Source | Description |
|---|---|---|
| 1 (highest) | Component Document tokens |
Tier 3 tokens. |
| 2 | Theme Document tokens |
Tier 2 tokens. |
| 3 (lowest) | Renderer defaults | Platform/implementation defaults. |
Resolution:
- When a
$token.keyreference is encountered in the Component Document, look upkeyin the Component Document'stokens. - If NOT found, look up
keyin the Theme Document'stokens. - If NOT found, use the renderer's default value.
This cascade allows Component Documents to override specific theme tokens while inheriting the rest.
When a $token. reference cannot be resolved through the cascade
(not found in Component tokens, Theme tokens, or renderer defaults):
- The processor MUST use a reasonable platform-appropriate default.
- The processor SHOULD emit a warning identifying the unresolved token reference.
- The processor MUST NOT fail or halt rendering.
Web renderers SHOULD emit resolved theme tokens as CSS custom properties on the form's root container element. The recommended naming convention is:
--formspec-{token-key-with-dots-replaced-by-hyphens}
For example, a theme token color.primary with value #005ea2 SHOULD
be emitted as:
--formspec-color-primary: #005ea2;This enables external CSS — including design-system bridge stylesheets
and author-defined overrides — to reference theme tokens without
JavaScript coupling. Bridge CSS can use var(--formspec-color-primary)
to stay in sync with the active theme.
Renderers that emit CSS custom properties SHOULD update them when the
theme document changes. When updating properties after a theme change,
renderers SHOULD remove properties from the previous theme that are not
present in the new theme, to prevent stale values from affecting
styling. Renderers MAY also emit tokens from the Component Document's
tokens map, with component tokens taking precedence over theme tokens
for identically named properties.
Non-web renderers (PDF, native) MAY ignore this convention entirely.
This section defines how Tier 3 (Component Documents) interacts with Tier 2 (Themes) and Tier 1 (Definition presentation hints) in a multi-tier presentation stack.
When an item in the Definition is NOT bound to any component in the tree, the renderer falls back to lower tiers:
- Tier 2 (Theme): If a Theme Document is present and defines a widget/layout for the unbound item (via selectors or per-item overrides), use the theme's configuration.
- Tier 1 (Definition hints): If no Theme applies, use the
item's
presentationhints (core §4.2.5) to select a widget and configure rendering. - Renderer defaults: If no hints are available, use the
renderer's default widget for the item's
dataType.
Fallback rendering for unbound items follows the rules in §4.5.
Component Documents inherit tokens from an associated Theme Document.
When a Component Document references $token.color.primary and does
NOT define that token in its own tokens map, the resolution falls
through to the Theme's tokens map (§10.3).
This enables a common pattern: the Theme defines the design system tokens (colors, spacing, typography), and the Component Document references them without redeclaring.
A Component Document MAY override specific tokens to customize the appearance without diverging from the theme entirely.
The general precedence rule for all presentation decisions:
| Priority | Tier | Effect |
|---|---|---|
| 1 (highest) | Tier 3 — Component Document | Component tree layout, component selection, style, and tokens override everything below. |
| 2 | Tier 2 — Theme Document | Widget configuration, selector cascade, tokens, and page layout apply to items NOT controlled by Tier 3. Tier 2 tokens are inherited by Tier 3 (§10.3). |
| 3 (lowest) | Tier 1 — Definition hints | Inline presentation and formPresentation hints serve as baseline defaults. |
Specific interactions:
- Widget selection: Tier 3 component type overrides Tier 2 widget
assignment, which overrides Tier 1
widgetHint. - Label display: Tier 1 item
labelis the source of truth. Context-specific labels use thelabelsmap on the Definition item. - Layout: Tier 3 component tree completely replaces Tier 2 page layout for bound items.
- Tokens: Tier 3 tokens override Tier 2 tokens of the same key; unoverridden tokens cascade from Tier 2.
- Behavioral rules:
required,readOnly,relevant,constraint, andcalculatefrom the Definition are never overridden by any presentation tier. They always apply.
A Component Document is NOT required to bind every item in the Definition. A partial tree binds only a subset of items. The remaining items are rendered via Tier 2/Tier 1 fallback (§11.1, §4.5).
This enables incremental adoption: an author can create a Component Document that controls the layout of key sections while allowing simpler fields to render automatically.
The renderer MUST:
- Render the component tree's output first.
- Identify all Definition items not bound in the tree.
- Render unbound visible items using fallback rules, appended after the tree output.
- Ensure all required items are editable, regardless of whether they appear in the tree.
This section defines the validation requirements for Component Documents and the conformance criteria for processors.
A conforming processor MUST validate a Component Document against
the structural rules defined in this specification. These rules MAY
be expressed as a JSON Schema (component.schema.json) for tooling
purposes.
Structural validation MUST verify:
- Required properties:
$formspecComponent,version,targetDefinition, andtreeare present. - Type correctness: Each property has the correct JSON type (string, object, array, integer, boolean) as specified.
- Enum constraints: Properties with enumerated values
(
direction,align,severity,position, etc.) contain valid values. - Component names: Every
componentvalue is either a built-in name (§5, §6) or a key in thecomponentsregistry. - Children constraints: Components that do not accept children
(§3.4) do not have a
childrenproperty. - ConditionalGroup
when: ConditionalGroup components include awhenproperty. - Heading props:
levelis 1–6 andtextis present.
Structural validation MUST be performed before referential integrity checks (§12.2).
After structural validation passes, processors MUST verify referential integrity:
-
Bind keys: Every
bindvalue MUST correspond to an itemkeyin the target Definition. Unknown bind keys MUST produce a warning. Processors SHOULD reject documents with bind keys that reference non-existent items, or MAY treat them as non-fatal warnings. -
Token references: Every
$token.keyreference SHOULD resolve to a token in the Component Document'stokensmap, the Theme Document'stokensmap, or be a well-known renderer default. Unresolvable token references MUST produce a warning (§10.4). -
Custom component references: Every
componentvalue that is not a built-in name MUST exist as a key in thecomponentsregistry. References to undefined custom components MUST be rejected. -
Custom component params: When instantiating a custom component, every param declared in the template's
paramsarray MUST have a corresponding entry in the instantiation'sparamsobject. Missing params MUST be rejected. -
Summary and DataTable bind refs: The
bindvalues within Summaryitemsand DataTablecolumnsMUST reference valid item keys. -
Cycle-free custom components: The custom component dependency graph MUST be acyclic (§7.4).
Processors MUST verify that each Input component's bound item has a
compatible dataType, per the matrix in §4.6:
- Look up the bound item's
dataTypein the Definition. - Check the component's compatible dataTypes list.
- If the dataType is NOT in the list, emit a validation error.
Incompatible bindings SHOULD be treated as errors. Processors MAY continue rendering with a warning, using the component as-is and relying on the renderer's type coercion, but this behavior is NOT RECOMMENDED.
A processor declares conformance at one of two levels:
Core Conformant:
- MUST parse and validate all Component Document properties defined in this specification.
- MUST render all 18 Core components (§5) with full prop support.
- MUST apply fallback substitution (§6.18) for all 17 Progressive components.
- MUST support custom component expansion (§7).
- MUST evaluate
whenexpressions (§8). - MUST support
responsiveprop overrides (§9). - MUST resolve
$token.references (§10). - MUST implement bind resolution rules (§4).
Complete Conformant:
- MUST satisfy all Core Conformant requirements.
- MUST additionally render all 17 Progressive components (§6) natively, without fallback substitution.
Processors SHOULD declare their conformance level in their documentation.
Formspec Component Documents are intentionally constrained to maintain predictability, portability, and ease of implementation. This section catalogues excluded features and the guard rails that keep Component Documents declarative.
The following features are explicitly excluded from this specification. They MUST NOT be implemented as normative behavior by conforming processors.
| Excluded Feature | Rationale |
|---|---|
| Imperative event handlers / scripting | Component Documents are declarative data, not programs. No onClick, onChange, or embedded JavaScript/FEL imperative code. |
| Conditional component type switching | The component prop MUST NOT vary by condition, breakpoint, or parameter. Structural ambiguity prevents static analysis. |
| Recursive custom components | Self-referencing templates produce unbounded trees. Prohibited and statically detected (§7.4). |
| Computed props via FEL | FEL is used ONLY in when conditions and display text interpolation. Props like columns, min, max MUST NOT be FEL expressions. |
| Arbitrary slot projection / transclusion | Components do not have named slots or content projection beyond children. This avoids the complexity of Angular/Vue-style slot APIs. |
| Animation specifications | Transitions, keyframes, and timing functions are out of scope. Renderers MAY animate independently. |
| Server-side data fetching | Component Documents MUST NOT trigger HTTP requests, API calls, or data loading. All data is provided by the Formspec Instance. |
| Component inheritance | No extends or prototype-chain mechanism for component types. Use custom components (§7) for reuse. |
| Dynamic component registration | The components registry is static. Components MUST NOT be added or removed at runtime. |
| Deep responsive (children swap) | Responsive overrides MUST NOT alter children, bind, or component type (§9.4). Only presentational props may vary. |
The following limits protect processors and authors from excessive complexity:
| Guard Rail | Limit | Enforcement |
|---|---|---|
| Total tree depth | SHOULD NOT exceed 20 levels. | Processors MAY reject deeper trees. MUST NOT enforce limits below 10. |
| Custom component nesting | SHOULD NOT exceed 3 levels of expansion. | Processors MAY reject deeper nesting. |
Single when per component |
Each component has at most one when expression. |
Use FEL and/or for compound conditions. |
| String-only params | Custom component params values MUST be strings. |
No objects, arrays, numbers, or booleans as param values. |
| No param interpolation in type names | {param} MUST NOT appear in the component property. |
Prevents dynamic dispatch. |
| Flat token map | Tokens are a single-level key-value map. | No nested token groups or computed tokens. |
| Editable uniqueness | At most one editable Input per item key (§4.3). | Prevents conflicting write paths. |
| Static tree | The component tree structure is fixed at authoring time. | when hides/shows but does not add/remove nodes. |
Vendor-specific or experimental features MAY be introduced using the
x- prefix convention:
-
Custom component names: Names starting with
x-(e.g.,x-MapPicker,x-SignaturePad) MAY be used in thecomponentsregistry. Conforming processors MUST NOT assign built-in semantics tox-prefixed names. -
Extension properties: Top-level properties starting with
x-are reserved for extensions. Processors MUST ignore unrecognizedx-properties. -
Custom style keys: Style object keys starting with
x-are vendor-specific. Processors MUST ignore unrecognizedx-style keys. -
Custom token prefixes: Token keys starting with
x-follow the same convention as theme-spec §3.5.
Extension features MUST NOT be required for correct rendering of
Core or Progressive components. An x- feature that is absent or
unsupported MUST NOT cause a processing failure.
This appendix is informative.
The following Component Document defines a multi-page layout for a
budget submission form. It targets a Definition with items for project
information, budget line items, and approval. Wizard-style navigation
is controlled by formPresentation.pageMode in the Definition, not
by the component tree structure.
{
"$formspecComponent": "1.0",
"url": "https://agency.gov/forms/budget-2025/components/wizard",
"version": "1.0.0",
"name": "budget-wizard",
"title": "Budget Form — Multi-Page Layout",
"description": "A three-step wizard-style layout for the annual budget submission form.",
"targetDefinition": {
"url": "https://agency.gov/forms/budget-2025",
"compatibleVersions": ">=1.0.0 <2.0.0"
},
"breakpoints": {
"sm": 576,
"md": 768,
"lg": 1024
},
"tokens": {
"color.primary": "#0057B7",
"color.error": "#D32F2F",
"color.surface": "#FFFFFF",
"color.success": "#2E7D32",
"spacing.sm": "8px",
"spacing.md": "16px",
"spacing.lg": "24px",
"border.radius": "6px",
"typography.body.family": "Inter, system-ui, sans-serif"
},
"components": {
"AddressBlock": {
"params": ["prefix", "title"],
"tree": {
"component": "Card",
"title": "{title}",
"children": [
{ "component": "TextInput", "bind": "{prefix}Street",
"placeholder": "Street address" },
{
"component": "Grid",
"columns": 3,
"gap": "$token.spacing.md",
"responsive": {
"sm": { "columns": 1 }
},
"children": [
{ "component": "TextInput", "bind": "{prefix}City" },
{ "component": "TextInput", "bind": "{prefix}State" },
{ "component": "TextInput", "bind": "{prefix}Zip" }
]
}
]
}
}
},
"tree": {
"component": "Stack",
"children": [
{
"component": "Page",
"title": "Project Information",
"description": "Enter basic details about your project.",
"children": [
{
"component": "Grid",
"columns": 2,
"gap": "$token.spacing.md",
"responsive": {
"sm": { "columns": 1 }
},
"children": [
{ "component": "TextInput", "bind": "projectName" },
{ "component": "TextInput", "bind": "projectCode" }
]
},
{
"component": "Grid",
"columns": 2,
"gap": "$token.spacing.md",
"responsive": {
"sm": { "columns": 1 }
},
"children": [
{ "component": "Select", "bind": "department",
"searchable": true },
{ "component": "Select", "bind": "fiscalYear" }
]
},
{ "component": "TextInput", "bind": "description",
"maxLines": 4, "placeholder": "Describe the project scope" },
{
"component": "AddressBlock",
"params": { "prefix": "project", "title": "Project Location" }
}
]
},
{
"component": "Page",
"title": "Budget Details",
"description": "Add line items and set the total budget.",
"children": [
{
"component": "DataTable",
"bind": "lineItems",
"columns": [
{ "header": "Description", "bind": "itemDescription" },
{ "header": "Category", "bind": "itemCategory" },
{ "header": "Amount", "bind": "itemAmount" }
]
},
{ "component": "Divider" },
{
"component": "Grid",
"columns": 2,
"gap": "$token.spacing.md",
"responsive": {
"sm": { "columns": 1 }
},
"children": [
{
"component": "MoneyInput",
"bind": "totalBudget",
"currency": "USD",
"style": {
"background": "#F0F6FF",
"borderColor": "$token.color.primary",
"borderWidth": "2px"
}
},
{
"component": "MoneyInput",
"bind": "contingency",
"currency": "USD"
}
]
},
{
"component": "Alert",
"severity": "info",
"text": "Total budget is automatically calculated from line items.",
"when": "$totalBudget > 0"
}
]
},
{
"component": "Page",
"title": "Review & Submit",
"description": "Review your submission before signing.",
"children": [
{
"component": "Summary",
"items": [
{ "label": "Project Name", "bind": "projectName" },
{ "label": "Department", "bind": "department" },
{ "label": "Fiscal Year", "bind": "fiscalYear" },
{ "label": "Total Budget", "bind": "totalBudget" },
{ "label": "Contingency", "bind": "contingency" }
]
},
{ "component": "Divider", "label": "Certification" },
{
"component": "Toggle",
"bind": "certify",
"onLabel": "I certify this information is correct",
"offLabel": "Not yet certified"
},
{
"component": "ConditionalGroup",
"when": "$certify = true",
"fallback": "Please certify the information above to proceed.",
"children": [
{
"component": "Signature",
"bind": "approverSignature",
"strokeColor": "#000",
"height": 150
}
]
}
]
}
]
}
}This example demonstrates:
- Stack with three Pages for multi-page layout (wizard behavior via
formPresentation.pageMode). - Custom component (
AddressBlock) for reusable address entry. - Responsive Grid that collapses to single-column on small screens.
- DataTable bound to a repeatable group (
lineItems). - Progressive components (MoneyInput, DataTable, Summary, Alert, Signature) with defined Core fallbacks.
- Conditional rendering (
whenon Alert and ConditionalGroup). - Design tokens referenced in gap and style properties.
- Cross-tier token cascade (component tokens override theme tokens).
This appendix is normative.
The following table lists all 35 built-in components with their classification and key characteristics.
| # | Component | Category | Level | Children | Bind | Description |
|---|---|---|---|---|---|---|
| 1 | Page | Layout | Core | Yes | Forbidden | Top-level page/section container. |
| 2 | Stack | Layout | Core | Yes | Forbidden | Flexbox vertical/horizontal stacking. |
| 3 | Grid | Layout | Core | Yes | Forbidden | Multi-column grid layout. |
| 4 | Spacer | Layout | Core | No | Forbidden | Empty spacing element. |
| 5 | TextInput | Input | Core | No | Required | Single/multi-line text input. |
| 6 | NumberInput | Input | Core | No | Required | Numeric input with stepper. |
| 7 | DatePicker | Input | Core | No | Required | Date/time/datetime picker. |
| 8 | Select | Input | Core | No | Required | Native dropdown or combobox; optional multi-select (multiple). |
| 9 | CheckboxGroup | Input | Core | No | Required | Multi-select checkboxes. |
| 10 | Toggle | Input | Core | No | Required | Boolean switch. |
| 11 | FileUpload | Input | Core | No | Required | File attachment upload. |
| 12 | Heading | Display | Core | No | Forbidden | Section heading (h1–h6). |
| 13 | Text | Display | Core | No | Optional | Static or data-bound text. |
| 14 | Divider | Display | Core | No | Forbidden | Horizontal rule separator. |
| 15 | SubmitButton | Display | Core | No | Forbidden | Form submission trigger button. |
| 16 | Card | Container | Core | Yes | Forbidden | Bordered surface grouping. |
| 17 | Collapsible | Container | Core | Yes | Forbidden | Expandable/collapsible section. |
| 18 | ConditionalGroup | Container | Core | Yes | Forbidden | Condition-based visibility group. |
| 19 | Columns | Layout | Progressive | Yes | Forbidden | Explicit column widths layout. |
| 20 | Tabs | Layout | Progressive | Yes | Forbidden | Tabbed navigation container. |
| 21 | Accordion | Layout | Progressive | Yes | Optional¹ | Collapsible section list. |
| 22 | RadioGroup | Input | Progressive | No | Required | Radio button single-select. |
| 23 | MoneyInput | Input | Progressive | No | Required | Currency-aware numeric input. |
| 24 | Slider | Input | Progressive | No | Required | Range slider control. |
| 25 | Rating | Input | Progressive | No | Required | Star/icon rating control. |
| 26 | Signature | Input | Progressive | No | Required | Drawn signature capture. |
| 27 | Alert | Display | Progressive | No | Forbidden | Status message banner. |
| 28 | Badge | Display | Progressive | No | Forbidden | Compact label badge. |
| 29 | ProgressBar | Display | Progressive | No | Optional | Visual progress indicator. |
| 30 | Summary | Display | Progressive | No | Forbidden | Key-value summary display. |
| 31 | ValidationSummary | Display | Progressive | No | Forbidden | Live or submit validation message panel. |
| 32 | DataTable | Display | Progressive | No | Optional² | Tabular repeatable data. |
| 33 | Panel | Container | Progressive | Yes | Forbidden | Side panel. |
| 34 | Modal | Container | Progressive | Yes | Forbidden | Dialog overlay. |
| 35 | Popover | Container | Progressive | Yes | Forbidden | Anchored contextual overlay. |
¹ Accordion bind is optional; when provided it MUST reference a repeatable group key (see §6.3).
² DataTable binds to a repeatable group key, not a field key.
This appendix is normative.
The following matrix shows which Input components are compatible with
each Definition dataType. A ✓ indicates compatibility. Components
marked (P) are Progressive; all others are Core.
| dataType | TextInput | NumberInput | DatePicker | Select | CheckboxGroup | Toggle | FileUpload | RadioGroup (P) | MoneyInput (P) | Slider (P) | Rating (P) | Signature (P) |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
string |
✓ | |||||||||||
number |
✓ | ✓ | ||||||||||
integer |
✓ | ✓ | ✓ | |||||||||
boolean |
✓ | |||||||||||
date |
✓ | |||||||||||
dateTime |
✓ | |||||||||||
time |
✓ | |||||||||||
choice |
✓ | ✓ | ||||||||||
multiChoice |
✓ | ✓ | ||||||||||
attachment |
✓ | ✓ |
Notes:
- Display components (Text, Heading, Summary, etc.) are compatible
with ALL dataTypes when used in read-only
bindmode. They are omitted from this matrix because they do not perform data editing. - MoneyInput is compatible with
numberandinteger. It adds currency formatting on top of NumberInput's capabilities. Authors SHOULD use MoneyInput when the Definition item has aprefixof"$","€", or similar currency indicator. - TextInput MAY be used as a universal fallback for any dataType in exceptional cases, but processors SHOULD warn about the type mismatch.
- Select on
multiChoiceMUST usemultipleso the value is an array; otherwise the binding does not match the item's data type.