FormField
since v8.2.0Validation-aware field wrapper. Composes the existing Label/HelpText/
ErrorMessage subcomponents and registers the control with the surrounding
FormProvider. Wires id / aria-describedby / aria-invalid /
aria-required onto the control purely through the (extended) FieldContext
— no cloneElement. See ADR-0004.
Install
Add the package and import the component.
pnpm add @hey-mike/tungstenpnpm add @hey-mike/tungstenimport { FormField } from '@hey-mike/tungsten';import { FormField } from '@hey-mike/tungsten';Preview
Same fixtures used by the visual-regression suite.
Usage
apps/docs/app/snapshots/form-field/page.tsx
import { VariantGrid } from '../_components/VariantGrid';
import { FormProvider, FormField, Input } from '@hey-mike/tungsten';
// FormField always lives inside a FormProvider. Each variant wraps its own
// provider so the snapshot captures the field in isolation. We show the states
// FormField renders declaratively without interaction: default, with help text,
// and required (visible marker). The error state requires a failed submit, so
// it's covered by the unit tests + recipes rather than this static snapshot.
export default function FormFieldSnapshot() {
return (
<VariantGrid
title="FormField"
variants={[
{
label: 'default',
node: (
<FormProvider>
<FormField name="email" label="Email">
<Input placeholder="you@example.com" />
</FormField>
</FormProvider>
),
},
{
label: 'with help',
node: (
<FormProvider>
<FormField name="email" label="Email" help="We never share it.">
<Input placeholder="you@example.com" />
</FormField>
</FormProvider>
),
},
{
label: 'required',
node: (
<FormProvider>
<FormField name="email" label="Email" required>
<Input placeholder="you@example.com" />
</FormField>
</FormProvider>
),
},
]}
/>
);
}
import { VariantGrid } from '../_components/VariantGrid';
import { FormProvider, FormField, Input } from '@hey-mike/tungsten';
// FormField always lives inside a FormProvider. Each variant wraps its own
// provider so the snapshot captures the field in isolation. We show the states
// FormField renders declaratively without interaction: default, with help text,
// and required (visible marker). The error state requires a failed submit, so
// it's covered by the unit tests + recipes rather than this static snapshot.
export default function FormFieldSnapshot() {
return (
<VariantGrid
title="FormField"
variants={[
{
label: 'default',
node: (
<FormProvider>
<FormField name="email" label="Email">
<Input placeholder="you@example.com" />
</FormField>
</FormProvider>
),
},
{
label: 'with help',
node: (
<FormProvider>
<FormField name="email" label="Email" help="We never share it.">
<Input placeholder="you@example.com" />
</FormField>
</FormProvider>
),
},
{
label: 'required',
node: (
<FormProvider>
<FormField name="email" label="Email" required>
<Input placeholder="you@example.com" />
</FormField>
</FormProvider>
),
},
]}
/>
);
}
Props
Surface specific to <FormField />.
| Prop | Type | Default | Description |
|---|---|---|---|
| name* | string | — | Field name — the key under which the value lives in the form. |
| label | ReactNode | — | Visible label. Rendered above the control and wired via htmlFor. |
| help | ReactNode | — | Secondary help text rendered below the control. |
| required | boolean | — | Marks the field required: visible marker + aria-required on the control. |
| validate | ((value: unknown) => string) | — | Per-field validation; returns an error string when invalid. |
| children* | ReactNode | — | The form control (Input, Select, Textarea, …). |
| className | string | — | — |
Used in recipes
Compositions from the /recipes reference that use this component.
- →
Multi-select with chips in a FormField
A multi-select Combobox composed inside FormField. Selected tags render as removable chips; Backspace removes the last. Shows the FieldContext boundary.
- →
DatePicker in a FormField
A DatePicker composed inside FormField + FormProvider. The trigger picks up the label association, required marker, and aria-describedby through FieldContext — the same boundary Input/Select use.
- →
Login form
Minimal email + password sign-in form built on FormProvider + FormField. Email is required and format-checked; the submit handler only fires onValid when both fields pass.
- →
Multi-step wizard
A two-step wizard sharing one FormProvider across steps. Step navigation is local recipe state; field values and validation stay owned by FormProvider — the thin layer is not extended for step state. Per-step Next validates only that step’s fields before advancing.
- →
Profile edit form
Pre-filled profile form using FormProvider defaultValues, mixing Input, Select, and Textarea controls — all wired through FormField with no per-control glue.
- →
Settings form
Account settings panel grouping several FormFields with help text under section headings. Shows FormField composing labelled controls in a denser, sectioned layout.
- →
Signup form
Account creation form demonstrating cross-field validation: the confirm-password field is checked against password via FormProvider’s form-level validate(values).