Field
Label, control, description and error as one accessible form field, with its error, disabled and grouped states.
Field and its parts give every form control the same structure: a label above, the control, an
optional description below, and an error when the value is not accepted. They do not validate:
your application decides what is valid and what the message says; the field shows it.
Used for sign-in and for service notifications.
// apps/docs/examples/ui/field-anatomy.tsx'use client';
import { Field, FieldDescription, FieldLabel,} from '@workspace/active-ui/components/primitives/field';import {Input} from '@workspace/active-ui/components/primitives/input';
/** * Label, control and description. `FieldLabel` does not associate itself: * `htmlFor` on the label and the same `id` on the control do. Clicking the * label moves the focus into the input. */export default function FieldAnatomy() { return ( <Field className="max-w-sm"> <FieldLabel htmlFor="anatomy-email">Email</FieldLabel> <Input id="anatomy-email" type="email" placeholder="name@example.com" aria-describedby="anatomy-email-description" /> <FieldDescription id="anatomy-email-description"> Used for sign-in and for service notifications. </FieldDescription> </Field> );}Usage#
import { Field, FieldDescription, FieldError, FieldLabel,} from '@workspace/active-ui/components/primitives/field';import {Input} from '@workspace/active-ui/components/primitives/input';
<Field> <FieldLabel htmlFor="email">Email</FieldLabel> <Input id="email" type="email" aria-describedby="email-description" /> <FieldDescription id="email-description">Used for sign-in.</FieldDescription></Field>;Anatomy#
Field: one control with its label, description and error. It rendersrole="group".Field: the label. It does not associate itself: give itLabel htmland the control the sameFor id.Field: help text below the control.Description Field: the error, announced as it appears (Error role="alert").FieldandContent Field: label and description beside a control, in a horizontal field.Title Field,Set FieldandLegend Field: a group of related fields under one legend.Group Field: a divider between fields in a group.Separator
Validation and the error state#
// apps/docs/examples/ui/field-validation.tsx'use client';
import {useState, type FormEvent} from 'react';
import {Button} from '@workspace/active-ui/components/primitives/button';import { Field, FieldDescription, FieldError, FieldLabel,} from '@workspace/active-ui/components/primitives/field';import {Input} from '@workspace/active-ui/components/primitives/input';
/** The application's own rule and message: the field only shows them. */function validateEmail(value: string): string | null { if (value.trim() === '') return 'Enter your email address.'; if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) { return 'Enter an email address like name@example.com.'; } return null;}
/** * Validation on submit, then on every change. `data-invalid` on `Field` * styles the whole field; `aria-invalid` on the control tells assistive * technology; `aria-describedby` points at the description and the error, * which `FieldError` announces (`role="alert"`). */export default function FieldValidation() { const [value, setValue] = useState(''); const [error, setError] = useState<string | null>(null); const [submitted, setSubmitted] = useState(false); const [sent, setSent] = useState(false);
const onSubmit = (event: FormEvent) => { event.preventDefault(); const message = validateEmail(value); setSubmitted(true); setError(message); setSent(message === null); };
return ( <form onSubmit={onSubmit} noValidate className="flex max-w-sm flex-col gap-4" > <Field data-invalid={error ? true : undefined}> <FieldLabel htmlFor="validation-email"> Email <span aria-hidden className="text-destructive"> * </span> <span className="sr-only">(required)</span> </FieldLabel> <Input id="validation-email" type="email" autoComplete="email" value={value} aria-invalid={error ? true : undefined} aria-describedby={ error ? 'validation-email-description validation-email-error' : 'validation-email-description' } onChange={(event) => { setValue(event.target.value); setSent(false); if (submitted) setError(validateEmail(event.target.value)); }} /> <FieldDescription id="validation-email-description"> We send the confirmation to this address. </FieldDescription> {error ? ( <FieldError id="validation-email-error">{error}</FieldError> ) : null} </Field> <div className="flex items-center gap-3"> <Button type="submit">Send confirmation</Button> {sent ? ( <span role="status" className="text-sm"> Sent to {value}. </span> ) : null} </div> </form> );}Three things together make an error visible and audible:
data-oninvalid Fieldgives the label and the error the error colour;aria-on the control marks it as invalid for assistive technology and colours its border;invalid aria-on the control names the description and the error, so a screen reader reads them with the field.describedby
Show the error after the reader submits or leaves the field, not on the first keystroke. A
required field says so in its label; the * is hidden from screen readers, and a visually hidden
"(required)" is read instead.
Other states#
Set when the account was created.
- Use at least 12 characters.
- Add at least one digit.
One message on the first working day of the month.
// apps/docs/examples/ui/field-states.tsx'use client';
import { Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldLegend, FieldSet, FieldTitle,} from '@workspace/active-ui/components/primitives/field';import {Input} from '@workspace/active-ui/components/primitives/input';import {Switch} from '@workspace/active-ui/components/primitives/switch';
/** * Disabled, several errors at once, a horizontal row for a switch, and a * group of fields under one legend. */export default function FieldStates() { return ( <div className="grid w-full max-w-2xl gap-8 sm:grid-cols-2"> <Field data-disabled> <FieldLabel htmlFor="states-organisation">Organisation</FieldLabel> <Input id="states-organisation" disabled defaultValue="Example Ltd" /> <FieldDescription>Set when the account was created.</FieldDescription> </Field>
<Field data-invalid> <FieldLabel htmlFor="states-password">Password</FieldLabel> <Input id="states-password" type="password" aria-invalid aria-describedby="states-password-errors" defaultValue="short" /> <FieldError id="states-password-errors" errors={[ {message: 'Use at least 12 characters.'}, {message: 'Add at least one digit.'}, {message: 'Use at least 12 characters.'}, ]} /> </Field>
<Field orientation="horizontal"> <FieldContent> <FieldTitle id="states-statement-title">Monthly statement</FieldTitle> <FieldDescription id="states-statement-description"> One message on the first working day of the month. </FieldDescription> </FieldContent> <Switch aria-labelledby="states-statement-title" aria-describedby="states-statement-description" /> </Field>
<FieldSet> <FieldLegend>Address</FieldLegend> <FieldGroup> <Field> <FieldLabel htmlFor="states-street">Street</FieldLabel> <Input id="states-street" autoComplete="address-line1" /> </Field> <Field> <FieldLabel htmlFor="states-city">City</FieldLabel> <Input id="states-city" autoComplete="address-level2" /> </Field> </FieldGroup> </FieldSet> </div> );}Disabled:
data-ondisabled Fielddims the label; the control needs its owndisabled.Several errors: pass
errorstoField; it removes duplicates and shows a list from two messages on.Error Horizontal:
orientation="horizontal"puts the control beside its label, for switches and checkboxes;Fieldholds the title and description.Content Grouped:
Fieldrenders a realSet <fieldset>withFieldas itsLegend <legend>.
What your application provides#
The validation rules and the messages, in the reader's language.
The ids that tie label, control, description and error together.
When to show an error: on submit, on leaving the field, or while typing.
Accessibility and keyboard#
Clicking the label focuses its control when
htmlandFor idmatch.Fieldis announced when it appears; keep it out of the page while there is no error.Error A disabled control is skipped by
Tab.
API#
Field#
Import from @workspace/
Props
- Type
"horizontal""responsive""vertical"null- Default
'vertical'
How the label and the control are arranged. vertical: the label above the control. horizontal: the control beside its label, for a switch or a checkbox; put the title and description in Field. responsive: vertical, and horizontal once the surrounding Field is at least the md container width.
Other props, such as the standard attributes of the underlying element, className and ref, are passed through.
FieldError#
Import from @workspace/
Props
- Type
({ message?: string; } | undefineโฆ
The messages to show when there are no children, for example the errors of a form library. Duplicate messages are shown once; one message is shown as text, two or more as a list. An entry without a message is not shown.
Other props, such as the standard attributes of the underlying element, className and ref, are passed through.
FieldLegend#
Import from @workspace/
Props
- Type
"label""legend"- Default
'legend'
'legend' for the heading of a group of fields; 'label' for a legend in the size of a field label, for example above a group of checkboxes.
Other props, such as the standard attributes of the underlying element, className and ref, are passed through.
Other parts#
FieldSeparator
Props
- Type
ReactNode
Other props, such as the standard attributes of the underlying element, className and ref, are passed through.
Parts that take only their elementโs attributes
Field: the attributes of its element, with className and ref.Label Field: the attributes of aDescription <p>, with className and ref.Field: the attributes of aGroup <div>, with className and ref.Field: the attributes of aSet <fieldset>, with className and ref.Field: the attributes of aContent <div>, with className and ref.Field: the attributes of aTitle <div>, with className and ref.
Label#
Field is built on Label, a styled <label>. Use Label on its own for a control
outside a field, such as a checkbox beside its text. It dims inside a disabled group
(data-), or after a disabled control that carries the peer class.
Parts that take only their elementโs attributes
Label: the attributes of a<label>, with className and ref.
The controls themselves are documented on their own pages: Input and Textarea.
This page adapts the Field documentation of shadcn/ui (MIT licence) to this package; see Third-party notices.