abaabil

Field

29th lightest of the forty, by js weight

The label, description and error wrapper for any form control, including your own. It never renders the control, which is the decision that keeps it small.

Example (a11y tier)

Tab into each control. A screen reader reads the label, then the description and the error together, because both are part of the control's aria-describedby.

We only use it to send the receipt.

Enter an address with an @ in it.

Notify me about

You can change these later.

Import

import Field, { Fieldset, Label } from 'abaabil/field'         // structure only
import Field, { Fieldset, Label } from 'abaabil/field/styled'  // + CSS
import Field, { Fieldset, Label } from 'abaabil/field/a11y'    // + aria-describedby, aria-invalid, generated id

The control is yours

The browser already ties a label to a control through htmlFor and id, and a legend to a group through <fieldset>. What it does not supply is the layout of label, help text and error around a control that is not this library's own. Input, textarea, select and combobox each carry that wiring in their a11y tier; Field is the same wiring with the control left open, so a date picker, a masked input or a third-party editor sits flush beside an abaabil input.

There is no as, no component, no registry of control types. The child is either an element, cloned with the id the label points at, or a function that receives one spreadable props object and wires the control itself:

<Field id="dob" label="Date of birth" error={error}>
  {(control) => <DatePicker {...control} value={value} onChange={setValue} />}
</Field>

At a11y that object holds id, aria-describedby, aria-invalid while there is an error, and required. An aria-describedby already on the element is merged in, not replaced.

The required mark comes from the control

The asterisk is drawn by the stylesheet from :has(:required), not from the prop. The prop only sets the native required attribute on the control, so the mark cannot say “required” over a control that is not, and a control you wired by hand shows it the moment it carries the attribute. Empty alt text on the pseudo-element keeps the asterisk out of the accessible name; the attribute already tells assistive tech.

The error is not a live region

That is on purpose. The error is announced with the control, as part of its description, when focus lands there, which is how a screen reader user meets it anyway. role="alert" on a message that is already in the page when it mounts announces nothing, and a form that re-renders with several errors at once would fire several alerts over each other. Announcing a failed submit is the form's job, with a focused error summary, not a field's.

Both Field and Fieldset warn in development without a label; a control carrying its own aria-label or aria-labelledby counts. Below a11y,id is required and the tiers render from a Server Component tree; a11y is client-side only because useId fills the id in.

Props

PropTypeDefaultTierDescription
idstring— (generated at a11y)allThe control's id; required below a11y. label points at it and the description and error ids are ${id}-description and ${id}-error. On Fieldset, the fieldset's id.
labelReactNode—allA real <label htmlFor> on Field; the <legend> on Fieldset.
hideLabelbooleanfalseallField. Visually hides the label; it stays a <label> in the accessibility tree.
descriptionReactNode—allHelp text.
errorReactNode—allError text under the control. Sets aria-invalid at a11y.
requiredbooleanfalseallField. Set as the native required attribute on the control.
childrenReactNode | function—allField. An element, cloned with the control props, or a function called with them.
classNamestring—allMerged with the base class.

Label takes any <label> prop and adds the class.