Field
The form-field scaffold — label, description, error, legend, separator and choice-card layouts in vertical, horizontal or responsive orientation.
- Status
- Since
0.1.0- Accessibility pattern
- role=group with fieldset and legend
Last updated
Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Field before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify fieldInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/fieldRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
The same command installs the registry items it composes: @vegastack/label, @vegastack/separator.
Usage
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
} from "@/components/ui/field";
<FieldSet>
<FieldLegend>Profile</FieldLegend>
<FieldGroup>
<Field>
<FieldLabel htmlFor="name">Full name</FieldLabel>
<Input id="name" />
<FieldDescription>This appears on invoices.</FieldDescription>
</Field>
</FieldGroup>
</FieldSet>;Anatomy
A field is a label, a control, optional helper text and an optional error, in that order.
FieldContent is the flex column that keeps a label and its description together when the control
sits beside them; it is not needed when there is no description.
Optional helper text.
Examples
Composition
FieldGroup
├── Field
│ ├── FieldLabel
│ ├── Input / Textarea / Switch / Select
│ ├── FieldDescription
│ └── FieldError
├── FieldSeparator
└── FieldField · FieldLabel · Input.
FieldGroup · FieldSeparator · Field.
Form
Field is the scaffold every form is built from: one FieldGroup, one Field per control, and a
horizontal Field for the actions.
Input
Textarea
Select
Select your department or area of work.
Slider
Set your budget range ($200 – 800).
Fieldset
FieldSet renders a real <fieldset>; FieldLegend its <legend>.
Checkbox
Radio
Switch
Ask for a code from your authenticator app on every sign-in.
Choice Card
Wrapping a Field in a FieldLabel turns the whole block into a clickable card, which tints when
the control inside it is checked. It works with RadioGroupItem, Checkbox and Switch.
Field Group
FieldGroup stacks fields; FieldSeparator divides them.
RTL
Every gap and inset is logical, so a right-to-left form mirrors without any per-field work.
Responsive Layout
- Vertical — the default: label, control, helper text, stacked.
- Horizontal —
orientation="horizontal"puts the label and the control on one row; pair it withFieldContentto keep descriptions aligned. - Responsive —
orientation="responsive"switches between the two on the@md/field-groupcontainer query, so the layout follows the form's own width rather than the viewport's.
Validation and Errors
Put data-invalid on the Field to switch the whole block into its error state; the control inside
it is marked aria-invalid for you. FieldError takes children, or an errors array it
de-duplicates into a list.
- Must be at least 8 characters.
- Must contain a number.
Footnote
variant="footnote" renders a centred 12px muted line with a small gap above. Use it for the
footer line under a form's submit button, such as the auth blocks' "Don’t have an account?" or
"Access is managed by your administrator."
<Field>
<Button type="submit">Sign in</Button>
<FieldDescription variant="footnote">
Access is managed by your administrator.
</FieldDescription>
</Field>Header action
FieldHeader puts a field-level action on the label row: the FieldLabel (or a FieldContent
of label and description) at the start and a FieldAction at the end, aligned on the label's
baseline. The row wraps on narrow widths with the action kept at the end, and the action's negative
block margin keeps a size="sm" button from making the row taller than the label. Use it for an
action on the whole field, such as "Add more" on an image list or "Use template" on a textarea;
the control's own actions stay inside the control.
<Field>
<FieldHeader>
<FieldLabel>Images</FieldLabel>
<FieldAction>
<Button variant="ghost" size="sm">
<PlusIcon aria-hidden />
Add more
</Button>
</FieldAction>
</FieldHeader>
<FieldDescription>
3 of 10 images · The first image is the thumbnail.
</FieldDescription>
</Field>3 of 10 images · The first image is the thumbnail.
Character counter
FieldCounter counts a text field's characters against its maxLength. It sits at the end of the
label row (FieldHeader), on the label's line, so it takes no room under the control, and it
stays hidden until the value nears the limit — from 80% of maxLength by default, or the share
threshold names — then reads 92/100 in small muted figures, and turns destructive at the
limit. Pass the same maxLength to the control so the browser enforces it. Inside a Field the
counter is a field description, so the Input or Textarea is described by it while it shows;
a screen reader hears it only when it crosses a threshold ("8 characters left", "Character limit
reached"), never on every keystroke.
<Field>
<FieldHeader>
<FieldLabel htmlFor="bio">Bio</FieldLabel>
<FieldCounter value={bio} maxLength={100} />
</FieldHeader>
<Textarea
id="bio"
maxLength={100}
value={bio}
onChange={(event) => setBio(event.target.value)}
/>
</Field>Automatic wiring
Inside a Field, the control needs no ids and no aria-* props. Field renders Base UI's Field
underneath, so the control is labelled by FieldLabel, described by the FieldDescription and
FieldError that are rendered right now, and marked aria-invalid when the Field carries
data-invalid. An explicit id or htmlFor still wins, and an explicit aria-describedby keeps
its ids first, with the Field's rendered message ids after it. While the Field is invalid its
control is aria-invalid, whatever the control's own prop says. A Slider takes its label and
messages on its thumb from the Field; its own id and aria-describedby stay on the slider root,
as Base UI's Slider places them. A control outside a Field gets no
Field wiring. Put one control in each Field: a second control inside the same Field would take
the same label and messages.
Shown on invoices.
Up to 40 characters.
States
The three orientations plus the invalid and disabled blocks in one frame.
The default orientation.
The whole block dims.
Playground
Try every orientation with a description, an error message and the disabled state, then copy the generated JSX.
<Field>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input id="email" type="email" placeholder="you@vegastack.com" />
</Field>API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
data-invalid | "false" | "true" | boolean | — | |
orientation | "horizontal" | "responsive" | "vertical" | null | — |
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "label" | "legend" | — |
| Prop | Type | Default | Description |
|---|---|---|---|
errors | ({ message?: string; } | undefined)[] | — |
| Prop | Type | Default | Description |
|---|---|---|---|
maxLength* | number | — | The limit — the same number as the control's own maxLength. |
value* | string | — | The field's current value; its length is counted the way maxLength counts it. |
threshold | number | 0.8 | The share of maxLength at which the counter appears, from 0 to 1. Below it nothing shows. |
FieldSet, FieldGroup, FieldContent, FieldLabel, FieldTitle, FieldDescription and
FieldSeparator add no props of their own — each accepts everything the underlying element accepts.
Accessibility
FieldSetandFieldLegendrender a real<fieldset>/<legend>, so a screen reader announces the group's name before every control inside it.Fieldis arole="group"on Base UI's Field, so the control inside it is labelled byFieldLabel, described by the renderedFieldDescriptionandFieldError, and markedaria-invalidfrom the Field'sdata-invalid— with no ids to keep in sync (API-26).FieldErrorisrole="alert"and carries a leading icon, so an error is never signalled by colour alone (A11Y-8), and its ink is thedestructivefamily's-textrole, which clears AA on the page (A11Y-13).- The choice card carries no focus ring: the control inside it wears the global 2px outline instead (FOC-12), so focus is always on the thing that actually takes it.
FieldCounteris a field description while it shows, so the control'saria-describedbynames it, and its polite status line speaks only when it crosses a threshold — near the limit and at it — never per keystroke. At the limit its ink is thedestructivefamily's-textrole.- Use
FieldSeparatorsparingly — each one is a section boundary a screen reader has to cross.
| Contract | States tested |
|---|---|
| Behaviour | default, invalid, disabled, checked |
| Accessibility | native-or-base-ui-semantics, browser-accessibility-test, grouped, invalid, disabled |
| Visual | default, invalid, disabled, checked |
Do / Don't
Deviations
Upstream's file plus packages/ui/upstream/patches/field.patch. Every hunk:
- FOC-1, FOC-3, FOC-6, FOC-12 — the choice card's
has-[>[data-slot=field]]:has-[:focus-visible]:border-ringplusring-3 ring-ring/50is removed outright. Field restates no focus chrome of its own: a text control inside it darkens its own border subtly on focus and paints no fill (FOC-3, FOC-14's text-entry exception, MK 2026-09-27), and a checkbox, radio or switch inside it wears the global:focus-visiblebackground tint (FOC-12). The card's own border never changes colour on focus (FOC-14). - A11Y-8, A11Y-13, COL-12 —
FieldErrorgains a leadingCircleAlertIconso the error is not signalled by colour alone, and its ink becomestext-destructive-text, thedestructivefamily's text role. Upstream'stext-destructiveis the fill, which measures under AA as ink.Field's owndata-[invalid=true]:text-destructivemoves to the same role. - API-26 —
Field,FieldLabel,FieldDescriptionandFieldErrorrender Base UI'sField.Root,Field.Label,Field.DescriptionandField.Errorinside aField, anddata-invalidalso sets Base UI'sinvalid, so the control reads its label, description ids and invalid state from the Field. Outside aFieldeach part renders upstream's plain element.FieldTitletakes its ownfield-titleslot, so the Base UI label is the onefield-label. - DOC-2 —
cnis imported from@vegastack/design. - COL-24, INT-4 — hover and press come from the shared
state-layerutility, a translucent foreground layer (--state-hover,--state-pressed) over whatever fill is there, so the step reads the same on every surface in both themes; selected, open and highlighted fills arebg- state-selected/bg-state-hover. Operator, Regent, 2026-10-31.
Formula Input
A calculated field's formula on one line — field chips from a typed brace, × and ÷ as you type, the first row's result under it and its error in its place.
Field Grid
A responsive one-to-three column grid of Fields by container width, and FieldChoices — radios or checkboxes in a wrapping row.