Form Field
A presentational layout wrapper — label row, control slot, and hint/error text. Pure .astro with no runtime JS; drop any control into the slot. Error text replaces help text when present.
Preview
We will never share your email.
<FormField label="Email" helpText="We will never share your email.">
<input type="email" aria-label="Email" placeholder="you@example.com" />
</FormField> States
Mark a field required, surface an errorText in place of help text, or disabled the whole field.
The control is yours to style — this wrapper only owns the label, help and error
text. The plain <input> below is demo markup: its border comes
from a class in this page's own stylesheet, not from the design system. What the wrapper
does give a slotted control is the focus ring — an
errorText re-points --focus-ring-color to the error red, and
because custom properties inherit, that reaches a native control and an
esa-* component in a shadow root alike. Tab into Permit ID to see it.
For a real form, reach for Text Field
instead: it owns its label, help text, error text and border states, so you are not
hand-rolling three of the four.
Permit ID is required.
This field cannot be edited.
<FormField label="Project name" required>
<input type="text" aria-label="Project name" />
</FormField>
<FormField label="Permit ID" errorText="Permit ID is required." required>
<input type="text" aria-label="Permit ID" aria-invalid="true" />
</FormField>
<FormField label="Locked field" helpText="This field cannot be edited." disabled>
<input type="text" aria-label="Locked field" disabled value="Read only" />
</FormField> Sizes
The size prop scales the label; size the control yourself to match.
<FormField label="Extra small label" size="xs"> … </FormField>
<FormField label="Small label" size="sm"> … </FormField>
<FormField label="Medium label" size="md"> … </FormField>
<FormField label="Large label" size="lg"> … </FormField> API
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | '' | Label rendered above the control slot. |
size | 'xs' | 'sm' | 'md' | 'lg' | 'md' | Scales the label font size. |
helpText | string | '' | Helper text shown below the control. |
errorText | string | '' | Error message; replaces help text when present. |
required | boolean | false | Renders an asterisk on the label. |
disabled | boolean | false | Dims the field and blocks pointer events. |
The control goes in the default slot — the wrapper makes no assumptions about it.
Accessibility
- The wrapper is presentational only. Its
<label>has noforand does not wrap the slot, so it names nothing on its own — associate it yourself with afor/idpair. - That is only possible for a native control. The
esa-*Lit controls render into shadow DOM, and a light-DOMforcan never reach an input inside a shadow root. Around one of those, treat this label as a visual caption and set the control's ownlabelattribute instead — that one is associated. helpTextanderrorTextrender without anid, so they cannot be referenced byaria-describedbyeither, anderrorTextis not in a live region. Prefer the control's ownhelp-text/error-textwhere it has them.- The required asterisk is
aria-hidden, because it is decoration: it used to carryaria-label="required", which does nothing at all —aria-labelis ignored on a bare<span>(role=generic). The requirement itself must be set on the control, asaria-requiredor the nativerequiredattribute. Everyesa-*form control now does both when givenrequired. - Set
aria-invalid="true"on the slotted control whenevererrorTextis shown. disabledisopacity+pointer-events: none— it blocks the mouse only. The slotted control stays keyboard-focusable, operable, and still submits, so you must also disable it natively.- The wrapper adds no grouping semantics. Around a set of radios or checkboxes it renders a bare
<div>; the group needs its own name (see the forms contract in theaccessibilityskill).
Theming surface
Every public token this component reads, extracted from its source at build time,
split by blast radius: the hooks that belong to this component,
then the shared and system tokens it consumes alongside everything else. The
Resolves to column walks each token's real lineage —
component → semantic → primitive → raw value — so you
can see exactly where re-pointing it lands. A spoke re-skins by overriding these in
its theme-<slug>.css; component internals are never edited. No
hook for what you need? File it with /request-lego.
Type roles it names
This component does not set a size, weight, face or tracking of its own — it names a composite, and the composite carries all five. A composite is a class rather than a token, so none of it appears in the tables below; re-theme text by re-pointing the token family, not by editing the component.
| Role | Re-point | Reach |
|---|---|---|
.typography-body-md |
| +28 more componentsesa-app-shell, esa-breadcrumbs, esa-card, esa-checkbox, esa-checkbox-group, esa-collapsible, esa-combobox, esa-command-palette, esa-confirm-dialog, esa-container, esa-danger-zone, esa-dialog, esa-dropdown-menu, esa-empty-state, esa-entity-search, esa-header-nav, esa-input-tag, esa-nav-dropdown, esa-page-header, esa-popover, esa-progress-bar, esa-radio-group, esa-select, esa-side-dialog, esa-snackbar-item, esa-switch-toggle, esa-tab-layout, esa-textarea |
.typography-body-sm |
| +20 more componentsesa-alert-box, esa-breadcrumbs, esa-button-toggle, esa-card, esa-checkbox-group, esa-combobox, esa-command-palette, esa-date-picker, esa-empty-state, esa-file-upload, esa-input-tag, esa-link-column, esa-loading-overlay, esa-progress-bar, esa-radio-group, esa-search-panel, esa-select, esa-stat, esa-text-field, esa-textarea |
.typography-label-2xs |
| +11 more componentsesa-button-toggle, esa-checkbox-group, esa-color-picker, esa-combobox, esa-date-picker, esa-input-tag, esa-progress-bar, esa-radio-group, esa-range-slider, esa-text-field, esa-textarea |
.typography-label-lg |
| +12 more componentsesa-breadcrumbs, esa-button-toggle, esa-checkbox-group, esa-color-picker, esa-combobox, esa-date-picker, esa-input-tag, esa-radio-group, esa-range-slider, esa-select, esa-text-field, esa-textarea |
.typography-label-md |
| +33 more componentsesa-app-bar, esa-breadcrumbs, esa-button-group, esa-button-toggle, esa-card, esa-checkbox-group, esa-color-picker, esa-combobox, esa-command-palette, esa-confirm-dialog, esa-danger-zone, esa-date-picker, esa-dialog, esa-empty-state, esa-entity-search, esa-file-upload, esa-filter-container, esa-header-nav, esa-input-tag, esa-link-column, esa-page-header, esa-pillbox, esa-popover, esa-progress-bar, esa-radio-group, esa-range-slider, esa-select, esa-side-dialog, esa-sidebar-nav, esa-stat, esa-text-field, esa-textarea, esa-tooltip |
.typography-label-xs |
| +14 more componentsesa-breadcrumbs, esa-button-toggle, esa-checkbox-group, esa-color-picker, esa-combobox, esa-date-picker, esa-entity-search, esa-input-tag, esa-progress-bar, esa-radio-group, esa-range-slider, esa-select, esa-text-field, esa-textarea |
Wired to this component
None. This component has no hooks of its own — it themes entirely through the shared tokens below, so every override here also moves its siblings.
Shared tokens it reads
Owned by a family or by the semantic layer. Still overridable — but scope the
override (esa-foo { --form-border-color: … }) unless you mean to move
everything that reads it. Reach says what else that is.
| Token | Reach | Resolves to |
|---|---|---|
--form-error-border-color | component forms group · +9 more | #ce2c31 via --color-background-utility-danger → --color-red-11 |
--form-error-color | component forms group · +9 more | #ce2c31 via --color-content-utility-danger → --color-red-11 |
--form-help-color | component forms group · +9 more | #646464 via --color-content-default-secondary → --color-gray-11 |
--form-help-gap | component forms group · +5 more | 0.25rem via --spacing-100 |
--form-label-color | component forms group · +7 more | #646464 via --color-content-default-secondary → --color-gray-11 |
--form-label-gap | component forms group · +2 more | 0.25rem via --spacing-100 |
--color-content-utility-danger | semantic every component | #ce2c31 via --color-red-11 |