Prototype Library

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.

Inherited .astro import EsaFormField from '@esa/ecology/esa-form-field.astro';

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

PropTypeDefaultDescription
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 no for and does not wrap the slot, so it names nothing on its own — associate it yourself with a for/id pair.
  • That is only possible for a native control. The esa-* Lit controls render into shadow DOM, and a light-DOM for can never reach an input inside a shadow root. Around one of those, treat this label as a visual caption and set the control's own label attribute instead — that one is associated.
  • helpText and errorText render without an id, so they cannot be referenced by aria-describedby either, and errorText is not in a live region. Prefer the control's own help-text / error-text where it has them.
  • The required asterisk is aria-hidden, because it is decoration: it used to carry aria-label="required", which does nothing at all — aria-label is ignored on a bare <span> (role=generic). The requirement itself must be set on the control, as aria-required or the native required attribute. Every esa-* form control now does both when given required.
  • Set aria-invalid="true" on the slotted control whenever errorText is shown.
  • disabled is opacity + 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 the accessibility skill).