FormField

Label, hint, and error wrapper for controls that don't wire their own accessibility.

moleculesSource
import { FormField } from '@elirobinson/react/components/molecules/FormField';

Styles: @elirobinson/react/styles/molecules/FormField.css — already included when you import @elirobinson/react/styles.css.

Shown on client invoices.

Show code
import { FormField } from '@elirobinson/react/components/molecules/FormField';

export default function Basic() {
  return (
    <FormField label="Studio name" htmlFor="studio-name" hint="Shown on client invoices.">
      {(fieldProps) => <input id="studio-name" className="ds-input" {...fieldProps} />}
    </FormField>
  );
}

When to use it

Use FormField when you're labelling a control that doesn't wire its own label, hint, and error — a plain <input>, a native <select>, a third-party widget. It owns the <label> and message markup, and hands the control a bundle of accessibility props (aria-describedby, aria-invalid, aria-required) through a render prop.

Never wrap Input or Textarea in FormField. Both already render their own label and message region — nesting them produces two labels for one control. Reach for FormField specifically when the control underneath doesn't do that wiring itself.

With an error

error and hint are mutually exclusive in the rendered output — when both are set, the error wins and the hint is hidden. required adds a visible * next to the label (hidden from screen readers, since aria-required on the control already says it).

Enter a valid email address

Show code
import { FormField } from '@elirobinson/react/components/molecules/FormField';

export default function WithError() {
  return (
    <FormField
      label="Client email"
      htmlFor="client-email"
      error="Enter a valid email address"
      required
    >
      {(fieldProps) => (
        <input id="client-email" type="email" className="ds-input" {...fieldProps} />
      )}
    </FormField>
  );
}

Wrapping an arbitrary control

FormField's render prop works with anything that accepts aria-describedby, aria-invalid, and aria-required — not just <input>. Here it labels a native <select>.

How long clients have to pay once you send an invoice.

Show code
import { FormField } from '@elirobinson/react/components/molecules/FormField';

export default function CustomControl() {
  return (
    <FormField
      label="Invoice terms"
      htmlFor="invoice-terms"
      hint="How long clients have to pay once you send an invoice."
    >
      {(fieldProps) => (
        <select id="invoice-terms" className="ds-input ds-select" {...fieldProps}>
          <option value="net-15">Net 15</option>
          <option value="net-30">Net 30</option>
          <option value="due-on-receipt">Due on receipt</option>
        </select>
      )}
    </FormField>
  );
}

Props

PropTypeDefaultDescription
htmlForrequiredstring
labelrequiredstring
errorstring
hintstring
requiredboolean

Also accepts all Omit<HTMLAttributes<HTMLDivElement>, 'children'> props.

Accessibility

  • Renders a <Label htmlFor={htmlFor}> and calls children with a { 'aria-describedby', 'aria-invalid', 'aria-required' } bundle — spread that bundle onto the control you render inside.
  • FormField does not generate or set the child's id for you. You're responsible for giving the control an id that matches htmlFor — that's what makes clicking the label focus the field.
  • aria-describedby is only set when hint or error is present; both share one generated id, since only one message renders at a time.
  • The * appended to a required label is aria-hidden="true" — the accessible requirement signal is aria-required="true" on the control itself, not the asterisk.
  • Keyboard: no custom behavior of its own — once the id/htmlFor pair is wired correctly, clicking the label focuses the control via native browser behavior.
  • The ref forwards to the outer <div>.

Do

  • Keep the child id and htmlFor in sync by hand — FormField does not generate one for you.
  • Spread the full fieldProps bundle onto the control, even the props you think you don't need.
  • Reach for FormField specifically for controls that don't label themselves — native <select>, a range input, a third-party widget.
  • Write error text that says what to do, not just that something is wrong.

Don't

  • Nest Input or Textarea inside FormField — they already render their own label and message region.
  • Set both hint and error and expect to see both — only error renders.
  • Skip the id on the child control; without it the label has nothing to point to.
  • Use required without also making the control itself enforce it — the asterisk is a visual cue, not validation.