Form

Groups fields, collects their values, and handles submission with consistent validation and error announcement on every platform.

Basic usage

Install the package:

pnpm add @design-schema/react @design-schema/tokens

Import the stylesheet once for the app, then the component:

import '@design-schema/tokens/calm-precise/css';
import '@design-schema/react/index.css';
import { Form } from '@design-schema/react';

Examples

import { Form, Input, Button, Stack } from '@design-schema/react';

const signInFields = (
  <Stack gap="normal">
    <Input label="Email" name="email" type="email" required />
    <Input label="Password" name="password" type="password" required />
  </Stack>
);

const signInActions = <Button label="Sign in" type="submit" />;

<Form name="sign-in" label="Sign in" actions={signInActions}>{signInFields}</Form>

Props

8 rows
PropTypeDefaultDescriptionPlatforms
children requiredcontent—

Fields (Input etc.) and layout (Stack). The action row goes in `actions`. The examples describe children and actions in words; their stories render the named Inputs in a Stack at its default gap (`normal`) and the named Button bare in `actions`.

web, lit, rn, swiftui
actions requiredcontent—

The action row: at least one Button with `type: submit`, primary first (Form's action-order rule). Rendered after the fields with the form gap; a named slot on Lit and the `actions` anatomy part on every platform. A single action renders bare; two or more go in a horizontal Stack the consumer supplies. The actions part aligns its content to the inline start, so a single bare action keeps its natural width rather than stretching.

web, lit, rn, swiftui
namestring—

Identifier for the form, used for analytics and as the base of generated ids. React Native has no ids and focuses by ref, so it is inert there and exists for parity. An unnamed form bases its ids on a generated unique id (useId on web); two forms given the same `name` on one page is an authoring error Form does not detect.

web, lit, rn, swiftui
labelstring—

Accessible name for the form landmark, e.g. "Sign in". Required when a page has more than one form and `labelledBy` is not set. Nothing enforces this at runtime and no dev warning is emitted.

Accessibility: Rendered as aria-label so the form is a named region.

web, lit, rn, swiftui
labelledBystring—

Id of a visible Heading that names the form. Wins over `label` when both are set.

Accessibility: Rendered as aria-labelledby.

web, lit
validatesubmit | blur | changesubmit

When field-level validation runs. `submit` is the least noisy; `blur` is the usual choice for longer forms. `change` validates on change only, not also on blur; after a failed submission every mode re-validates on blur and change, until a successful submission resets that state. Form owns the rule: on web and rn the context carries `submitFailed` beside `validateMode`, and a field validates on blur when the mode is `blur` or `submitFailed` is true, and on change when the mode is `change` or `submitFailed` is true; on Lit ds-form listens itself and calls the field's `checkValidity()`, and the field renders its own error.

web, lit, rn, swiftui
disabledbooleanfalse

Disables every field and action inside. Use while submitting. Each field and action dims itself with its own disabled style; the Form container applies no opacity of its own (it would compound) and only exposes the disabled state (accessibilityState.disabled on RN). Web and Lit put no disabled attribute on the form element (aria-disabled is not valid on role form); each field and action reports its own.

web, lit, rn, swiftui
errorSummarybooleantrue

When submission fails validation, render a summary of errors above the fields that links to each field. Each item's text is the field's own message verbatim (a field's own required message already names the field, "Email is required."), never prefixed with the label; a field that is invalid with an empty message shows its `label` instead, and its `name` when the label is empty too. Every field's registration carries its `label` for that fallback, on all three platforms. The summary appears only after a failed submission (not for errors that blur or change validation finds before one), shrinks as fields are fixed, and is removed by a successful submission. Items follow document order at the failed submit; an error found later by blur or change validation is appended at the end. An entry whose field has since unregistered (a Disclosure closed after the failed submit) stays as plain danger Text with no Link until the next validation replaces the errors. The plural locale is read at the failed submit. The summary box is a column with a solid border. A `FailedSubmit` story on web and Lit submits the sign-in example empty in its play function, so the summary's markup and contrast pair are checked.

Accessibility: The summary receives focus and is announced, so users find every error without hunting.

web, lit, rn, swiftui

Events

2 rows
EventTypeDescriptionPlatforms
onSubmitonSubmit / submit

Fired when the form is submitted and every field is valid. Receives the collected values keyed by field name: `Record<string, string | number | boolean | string[] | [number, number]>` on every platform — Input and RadioGroup contribute strings, Switch a boolean, Checkbox its `value` when checked, NumberInput and Slider a number, multi-select Listbox, Select and Combobox a string array, a range Slider or DatePicker a pair; an unchecked Checkbox, an unselected RadioGroup, an empty field (a null, empty-string or empty-array value) and a disabled field contribute no key at all.

web, lit, rn, swiftui
onInvalidonInvalid / invalid

Fired when submission is blocked by validation. Receives the errors keyed by field name.

web, lit, rn, swiftui

Accessibility

Implicit ARIA role: form.

Anatomy

containerfieldserrorSummaryactions

What generation guarantees

Visible focus — Keyboard focus is always drawn, from the theme’s focus tokens, and is never removed.

Contrast — Text and meaningful non-text contrast meets WCAG AA; the pairs are listed below.

Contrast pairs

1 row
ForegroundBackgroundLevel
color.foreground.dangercolor.background.subtleAA

This component does not animate, so it has no reduced-motion behaviour to describe.