Components · Forms
SchemaDrivenForm
A form rendered from a field schema: seven field types, required and validation rules, sections and conditional fields.
Booking form with sections and a conditional field
import { useState } from 'react';
import { SchemaDrivenForm, type SchemaDrivenFormSchema, type SchemaDrivenFormValues } from '@rookcom/monarch-react/patterns';
const BOOKING: SchemaDrivenFormSchema = [
{
type: 'section',
id: 'you',
title: 'Your details',
fields: [
{ type: 'text', name: 'name', label: 'Full name', required: true, autoComplete: 'name' },
{
type: 'text',
name: 'email',
label: 'Email',
inputType: 'email',
required: true,
autoComplete: 'email',
pattern: '[^@\\s]+@[^@\\s]+\\.[^@\\s]+',
patternMessage: 'Enter an email address, such as [email protected].',
},
],
},
{
type: 'section',
id: 'visit',
title: 'Your visit',
description: 'Tell us what to prepare.',
fields: [
{
type: 'select',
name: 'topic',
label: 'Topic',
required: true,
options: [
{ value: 'demo', label: 'Product demo' },
{ value: 'support', label: 'Support question' },
{ value: 'other', label: 'Something else' },
],
},
{
type: 'textarea',
name: 'other',
label: 'What would you like to discuss?',
required: true,
maxLength: 500,
visibleWhen: { field: 'topic', equals: 'other' },
},
{ type: 'number', name: 'guests', label: 'Guests', min: 1, max: 10, integer: true },
{ type: 'date', name: 'date', label: 'Preferred date', min: '2026-01-01' },
{
type: 'switch',
name: 'reminder',
label: 'Email reminder',
description: 'One day before.',
defaultValue: true,
},
{
type: 'checkbox',
name: 'consent',
label: 'I agree to be contacted about this booking',
required: true,
},
],
},
];
function BookingDemo() {
const [sent, setSent] = useState<SchemaDrivenFormValues | null>(null);
return (
<div style={{ display: 'grid', gap: 16, width: '100%', maxWidth: 520 }}>
<SchemaDrivenForm
aria-label="Booking form"
schema={BOOKING}
locale="en-GB"
submitLabel="Book"
onSubmit={async (values) => {
await new Promise((r) => setTimeout(r, 700));
setSent(values);
}}
/>
{sent ? (
<pre style={{ margin: 0, fontSize: 12, whiteSpace: 'pre-wrap' }}>
{JSON.stringify(sent, null, 2)}
</pre>
) : null}
</div>
);
}
<BookingDemo />Usage examples
Integration settings with a Cancel action
import { Button } from '@rookcom/monarch-react';
import { SchemaDrivenForm, type SchemaDrivenFormSchema } from '@rookcom/monarch-react/patterns';
const CONNECTION: SchemaDrivenFormSchema = [
{ type: 'text', name: 'url', label: 'Server URL', inputType: 'url', required: true },
{
type: 'text',
name: 'token',
label: 'Access token',
inputType: 'password',
required: true,
description: 'Found in your server’s admin settings.',
},
{
type: 'number',
name: 'timeout',
label: 'Timeout (seconds)',
min: 1,
max: 120,
defaultValue: 30,
},
{ type: 'checkbox', name: 'verifyTls', label: 'Verify TLS certificates', defaultValue: true },
];
<div style={{ width: '100%', maxWidth: 520 }}>
<SchemaDrivenForm
aria-label="Connection settings"
schema={CONNECTION}
submitLabel="Connect"
secondaryActions={<Button variant="ghost">Cancel</Button>}
/>
</div>Read-only
import { SchemaDrivenForm, type SchemaDrivenFormSchema } from '@rookcom/monarch-react/patterns';
const BOOKING: SchemaDrivenFormSchema = [
{
type: 'section',
id: 'you',
title: 'Your details',
fields: [
{ type: 'text', name: 'name', label: 'Full name', required: true, autoComplete: 'name' },
{
type: 'text',
name: 'email',
label: 'Email',
inputType: 'email',
required: true,
autoComplete: 'email',
pattern: '[^@\\s]+@[^@\\s]+\\.[^@\\s]+',
patternMessage: 'Enter an email address, such as [email protected].',
},
],
},
{
type: 'section',
id: 'visit',
title: 'Your visit',
description: 'Tell us what to prepare.',
fields: [
{
type: 'select',
name: 'topic',
label: 'Topic',
required: true,
options: [
{ value: 'demo', label: 'Product demo' },
{ value: 'support', label: 'Support question' },
{ value: 'other', label: 'Something else' },
],
},
{
type: 'textarea',
name: 'other',
label: 'What would you like to discuss?',
required: true,
maxLength: 500,
visibleWhen: { field: 'topic', equals: 'other' },
},
{ type: 'number', name: 'guests', label: 'Guests', min: 1, max: 10, integer: true },
{ type: 'date', name: 'date', label: 'Preferred date', min: '2026-01-01' },
{
type: 'switch',
name: 'reminder',
label: 'Email reminder',
description: 'One day before.',
defaultValue: true,
},
{
type: 'checkbox',
name: 'consent',
label: 'I agree to be contacted about this booking',
required: true,
},
],
},
];
<div style={{ width: '100%', maxWidth: 520 }}>
<SchemaDrivenForm
schema={BOOKING}
readOnly
locale="en-GB"
defaultValues={{
name: 'Amara Okafor',
email: '[email protected]',
topic: 'demo',
guests: 3,
date: '2026-03-12',
consent: true,
}}
/>
</div>Purpose
Renders forms whose fields are data rather than code: booking and intake forms people build, custom fields, integration settings defined by a provider list. The product owns the schema and what happens on submit. Import from @rookcom/monarch-react/patterns.
Use when
- The fields come from configuration or the server (custom fields, form builders, integration settings).
- The same fields must render identically in a builder preview and the live form.
Avoid when
- A fixed form written once: compose Field, Input and friends directly, so each field can be tuned.
- Settings that save as each switch flips: use SwitchSettingsList.
Design principles
- Small and typed: a discriminated union of seven field types, no form library, no runtime schema parser.
- The schema is data: conditions such as
{ field: "topic", equals: "other" }can come from JSON; a function is allowed when data cannot say it. - Built on Field, so every label, help text and error is wired the same way as hand-written Monarch forms.
- Errors appear when a field is left or on submit, never while someone is still typing their first character.
- Hidden fields are neither validated nor submitted, so a condition can never block the form invisibly.
API reference
import { SchemaDrivenForm, formatSchemaValue, isSchemaFieldVisible, schemaDefaultValues, validateSchemaField, validateSchemaForm } from '@rookcom/monarch-react/patterns';| Export | Kind | Rendering | Root class |
|---|---|---|---|
SchemaDrivenForm | component | client | .mn-schema-driven-form |
formatSchemaValue | utility | client | – |
isSchemaFieldVisible | utility | client | – |
schemaDefaultValues | utility | client | – |
validateSchemaField | utility | client | – |
validateSchemaForm | utility | client | – |
Renders a <form noValidate> (ref); sections are <fieldset> with a <legend>. Each field wrapper has data-schema-field and data-type.
Fields: text (with inputType email, url, tel or password), textarea, number, select, checkbox, switch and date. Values are strings, numbers, booleans, YYYY-MM-DD strings or null.
Rules: required, minLength, maxLength, pattern (a string, anchored), min, max, integer and a validate(value, values) function.
Helpers: schemaDefaultValues, isSchemaFieldVisible, validateSchemaField, validateSchemaForm (reuse them on the server) and formatSchemaValue for read-only display.
Generated from the TypeScript declarations of packages/react/src/components/forms/schema-driven-form.
Properties
SchemaDrivenForm
Renders a form from a field schema: text, number, select, checkbox, switch, date and textarea fields, with required and length, pattern and range rules, titled sections and conditional visibility. Built on Field, so labels, help and errors are wired for assistive technology. On submit it validates, focuses the first invalid field, and passes the visible values.
| Prop | Type | Description |
|---|---|---|
schema required | SchemaDrivenFormSchema | The fields and sections to render, in order. |
className | string | |
defaultValues | SchemaDrivenFormValues | Initial values (uncontrolled). Default: each field's `defaultValue`. |
disabled | boolean | Disables every field and the submit button. |
errors | Record<string, ReactNode> | Errors from the product, such as the server's, keyed by field name. Shown with the built-in ones. |
id | string | |
locale | string | BCP 47 locale for dates and numbers. Pass it for server rendering. |
messages | Partial<SchemaDrivenFormMessages> | Override built-in strings. |
onSubmit | ((values: SchemaDrivenFormValues) => unknown) | Called with the visible fields' values once they are valid. Return a promise to show the submit button busy; reject with an Error to show its message above the buttons. |
onValuesChange | ((values: SchemaDrivenFormValues, name: string) => void) | Called on every change with all values and the name of the field that changed. |
readOnly | boolean | Show values as text in a description list instead of controls (no `<form>`, so no ref). |
secondaryActions | ReactNode | Buttons after submit, such as Cancel. |
submitLabel | ReactNode | Label of the submit button, or `null` for no button (submit from outside with `form=`). |
validateOn | "submit" | "blur" | When errors first show: when a field loses focus (`blur`, default) or only on submit. |
values | SchemaDrivenFormValues | Values (controlled), keyed by field name. Missing keys use the field's empty value. |
Variants
| Prop | Values | Description |
|---|---|---|
validateOn | 'blur''submit' | blur (default) shows a field’s error once it loses focus; submit waits for the first submit. |
readOnly | 'false''true' | true shows the visible values as a description list, formatted with formatSchemaValue. |
submitLabel | '“Save” (default)''any text''null' | null hides the button, for a submit button elsewhere (form attribute) or a dialog footer. |
States
| State | Exposed as | Behaviour |
|---|---|---|
| Invalid | aria-invalid on the control | Error under the field, linked by aria-describedby. |
| Required | aria-required | Asterisk after the label. |
| Hidden | – | A field or section whose visibleWhen does not hold is not rendered. |
| Submitting | aria-busy on the form and button | Fields disabled while the onSubmit promise runs. |
| Submit failed | role="alert" | The rejection’s message above the buttons. |
| Disabled | – | Every field and the submit button disabled. |
| Read-only | data-readonly | A description list instead of controls. |
Interaction behaviour
- Each change calls
onValuesChange(values, name); conditional fields appear and disappear immediately. - Submit validates every visible field; if any fail, focus moves to the first invalid one and
onSubmitis not called. - Valid submits call
onSubmitwith only the visible fields’ values. Return a promise to show the busy state; reject to show an error. - Number fields keep partial text such as “-” or “1.” while the value stays a number.
- Product errors (for example from the server) go in
errorsby field name and show at once.
Keyboard support
| Keys | Action |
|---|---|
| Tab / Shift+Tab | Moves between fields and buttons in schema order. |
| Enter | In a single-line field, submits the form. |
| Space | Toggles a checkbox or switch. |
The date field adds DatePicker’s calendar keys; selects use the platform’s keys.
Accessibility
- Role
- form (when named) containing groups
- Accessible name
- Give the form an
aria-labeloraria-labelledby; sections are named by their legend.
- Labels, descriptions and errors are wired by Field; required fields have
aria-requiredand a visual asterisk. - Section descriptions are linked to their fieldset with
aria-describedby. - Errors are announced politely when they appear; a failed submit is an alert.
- Focus moves to the first invalid field on submit, so keyboard and screen-reader users land on the problem.
- Verified with axe in jsdom and in Chromium, Firefox and WebKit on /components/schema-driven-form.
Responsive behaviour
- One column at every width; controls fill the form’s width, number fields are capped at 12rem.
- The read-only list puts labels beside values, and stacks them under 30rem.
Theme support
- Monarch tokens only; sections are separated by
--mn-color-border-subtle. - Every control is a Monarch control, so light, dark, high-contrast and product accents apply unchanged.
Do and don't
Do
- Validate again on the server with the same schema (
validateSchemaFormruns anywhere). - Write
patternMessagewhenever you usepattern, with an example of the expected format. - Pass
localewhen rendering on the server, so dates and numbers match the client.
Don't
- Do not hide a required field behind a condition and expect it to be filled; hidden fields are skipped.
- Do not use a switch for consent; use a required checkbox.
- Do not reach for this for one fixed form; hand-written Fields are easier to tune.
Related components
- Field: The label, help and error wiring every field uses.
- DatePicker: The date field.
- SwitchSettingsList: Settings that apply as each switch flips.
- DescriptionList: Read-only label and value pairs in other layouts.
Dependencies
Testing coverage
schemaDefaultValues, data and function conditions withisSchemaFieldVisible.validateSchemaForm: required, must-accept, pattern, integer, maxLength, max and number rules; hidden fields skipped.formatSchemaValue: option labels, Yes/No, empty, locale numbers and dates.- Labelled controls of every type in named fieldsets with descriptions; conditional field hidden; axe.
- Error on blur, cleared by typing; conditional field appears; axe.
- Submit validates, focuses the first invalid field, then submits only visible values.
- Number text keeps partial input and reports text that is not a number.
- Controlled values; product errors; rejected submit alert; secondary actions.
- Busy while submitting; disabled; no submit button with
submitLabel={null}. - Read-only description list; axe; translated messages; renders on the server.
| Check | Covered |
|---|---|
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery/schema-driven-form; Cross-browser axe: /components/schema-driven-form |
Tests: packages/react/src/components/forms/schema-driven-form