Form
Anything submitted: a Base UI Form, one column at --size-sm, with an error summary at the top and an action row at the end. Instant-apply settings are switches, not forms.
import {
Form,
FormActions,
FormRow,
FormSummary,
} from '@fairgarden-private/design/components/Form'
The root is the inline-size container form: short pairs share a row from 480 px of form width, framed fieldsets tighten below 360 px, and a butted submit turns icon-only there. Base UI validates on submit and then re-validates failed fields as they change.
Summary, pairs and actions
Summary, pairs and actions
FormSummary is the danger Alert with a type-subhead heading and a link to each failing field; render it only while there are errors, and it takes focus when it appears. FormRow holds a short pair in a 2:1 split. In FormActions, author the solid submit last: it shows first and full width at base and rightmost from --md-n-above. busy makes the form inert while the submit reads "Sending…".
'use client'
import * as React from 'react'
import { Button } from '@fairgarden-private/design/components/Button'
import { Field, FieldError, FieldLabel } from '@fairgarden-private/design/components/Field'
import {
Form,
FormActions,
FormRow,
FormSummary,
} from '@fairgarden-private/design/components/Form'
import { Input } from '@fairgarden-private/design/components/Input'
type Errors = Record<string, string>
/**
* Submit with empty fields to see the summary take focus and the fields
* take the error state; a valid submit shows the busy form for a moment.
*/
export function FormStates() {
const [errors, setErrors] = React.useState<Errors>({})
const [busy, setBusy] = React.useState(false)
const submit = (values: Record<string, unknown>) => {
const next: Errors = {}
if (!values.email) next.email = 'Enter an email address, like name@example.com.'
if (!values.postcode) next.postcode = 'Enter a postcode.'
setErrors(next)
if (Object.keys(next).length > 0) return
setBusy(true)
window.setTimeout(() => setBusy(false), 1500)
}
const count = Object.keys(errors).length
return (
<Form errors={errors} onFormSubmit={submit} busy={busy}>
{count > 0 ? (
<FormSummary
title={`Fix ${count} ${count === 1 ? 'field' : 'fields'} to continue`}
errors={Object.entries(errors).map(([name, message]) => ({
id: `form-demo-${name}`,
message,
}))}
/>
) : null}
<Field name="email">
<FieldLabel>Email Address</FieldLabel>
<Input id="form-demo-email" type="email" placeholder="Email address…" />
<FieldError />
</Field>
<FormRow>
<Field name="city">
<FieldLabel optional>City</FieldLabel>
<Input id="form-demo-city" />
</Field>
<Field name="postcode">
<FieldLabel>Postcode</FieldLabel>
<Input id="form-demo-postcode" />
<FieldError />
</Field>
</FormRow>
<FormActions>
<Button type="reset" size="lg">
Clear
</Button>
<Button type="submit" variant="solid" size="lg" aria-busy={busy || undefined}>
{busy ? 'Sending…' : 'Join the Walk'}
</Button>
</FormActions>
</Form>
)
}
Primary scale
Primary scale
The Form's primary reaches every field inside it; secondary is accepted and unused.
import { Button } from '@fairgarden-private/design/components/Button'
import { Field, FieldLabel } from '@fairgarden-private/design/components/Field'
import { Form, FormActions } from '@fairgarden-private/design/components/Form'
import { Input } from '@fairgarden-private/design/components/Input'
/** `primary` on the Form reaches every field inside it by inheritance. */
export function FormColor() {
return (
<Form primary="plum">
<Field>
<FieldLabel>Your Name</FieldLabel>
<Input />
</Field>
<FormActions>
<Button type="submit" variant="solid" size="lg">
Send
</Button>
</FormActions>
</Form>
)
}
On grounds
On paper and forest
On forest the field roles take the deep ground's map and the submit stays amber.
paper
forest
import { Button } from '@fairgarden-private/design/components/Button'
import { Field, FieldLabel } from '@fairgarden-private/design/components/Field'
import { Form, FormActions } from '@fairgarden-private/design/components/Form'
import { PresetGround } from '@/components/PresetGround'
import { Input } from '@fairgarden-private/design/components/Input'
import styles from './grounds.module.css'
const presets = ['paper', 'forest'] as const
/** A short form on paper and forest; the submit keeps the scope's action. */
export function FormGrounds() {
return (
<div className={styles.row}>
{presets.map((preset) => (
<PresetGround key={preset} preset={preset} className={styles.face}>
<p className={styles.name}>{preset}</p>
<Form>
<Field>
<FieldLabel>Email Address</FieldLabel>
<Input type="email" />
</Field>
<FormActions>
<Button type="submit" variant="solid" size="lg">
Sign Up
</Button>
</FormActions>
</Form>
</PresetGround>
))}
</div>
)
}
API Reference
Form
A Base UI Form, one column at --size-sm. Base UI validates on submit,
then re-validates failed fields as they change (§10.2 asks for blur;
pass validationMode="onBlur" to a Field that should wait for it).
Order: an optional FormSummary, fields and fieldsets, then
FormActions.
| Prop | Type | Description |
|---|---|---|
busy | | While the submission is pending: the form is |
primary | | Primary Radix scale: structure and inherited control roles. Never defaulted [D133]. |
secondary | | Secondary Radix scale: accepted, unused. |
className | | Extra class names, added after the module’s own. |
FormSummary
The error summary at the top of the form (§10.2): the danger Alert with a heading and a link to each failing field. Render it only while there are errors; it takes focus when it appears.
| Prop | Type | Description |
|---|---|---|
errors | | The failing fields, each a link that moves focus to its control. |
title | | The heading, in |
className | | Extra class names, added after the module’s own. |
FormRow and FormActions
A short pair, such as city and postcode: a 2:1 row that stacks when both fields do not fit, and always shares the row from 480 px of form width.
The action row, --size-px-5 below the last field. Author the solid
submit (size="lg") last: it shows first and full width at base, and
last (rightmost) from --md-n-above [D106].
Additional types
FormActionsProps
Props for FormActions: div props.
type FormActionsProps = React.ComponentPropsWithRef<'div'>FormProps
Props for Form: Base UI Form props plus busy and the color axes.
type FormProps<Values extends Record<string, unknown> = Record<string, unknown>> = {
/** Extra class names, added after the module's own. */
className?: string;
/**
* While the submission is pending: the form is `inert` and
* `aria-busy`. Give the solid submit its "-ing…" label ("Sending…").
*/
busy?: boolean;
/** Primary Radix scale: structure and inherited control roles. Never defaulted [D133]. */
primary?:
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null;
/** Secondary Radix scale: accepted, unused. */
secondary?:
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| 'amber'
| 'blue'
| 'cyan'
| 'grass'
| 'green'
| 'jade'
| 'lime'
| 'mint'
| 'orange'
| 'sky'
| 'teal'
| 'yellow'
| null;
}FormRowProps
Props for FormRow: div props.
type FormRowProps = React.ComponentPropsWithRef<'div'>FormSummaryError
One failing field in the summary: the field’s id and the message that links to it.
type FormSummaryError = {
/** The `id` of the field's control; the link moves focus there. */
id: string;
/** The field's error message, as at the field. */
message: React.ReactNode;
}FormSummaryProps
Props for FormSummary.
type FormSummaryProps = {
/** The heading, in `type-subhead`: "Fix 2 fields to continue". */
title: React.ReactNode;
/** The failing fields, each a link that moves focus to its control. */
errors: FormSummaryError[];
/** Extra class names, added after the module's own. */
className?: string;
}Specification: DESIGN-SYSTEM.md §10.2 (field, fieldset and form) and §5.10.2 (the form's container thresholds).