Form

Layout, validation and state for a set of fields, with each field's label and error message linked by its name.

Accessibility

A named Form.Item does most of this for you by cloning its child. What follows is what it wires, what it cannot, and the cases where the wiring does not happen.

What Form.Item wires up for you

A named Form.Item clones its single child element and adds the attributes that tie a control to its label and its message. All of them key off the field id the Item derives from name, so they stay in step.
  • An id on the control. The Item sets id to the derived field id, but only when the child does not already carry one. A hand-written id wins, and breaks the pairing below if it does not match.
  • htmlFor on the label, pointing at that same id. Clicking the label focuses the control, and the label becomes the control's accessible name.
  • aria-invalid="true" while the field has errors. Warnings do not set it. A rule marked warningOnly paints the warning color and leaves the field valid, so do not use one for a problem that must be fixed.
  • aria-describedby pointing at the help block, <fieldId>_help, whenever there is a help node or at least one error. The help block carries that id itself, so the message is read out after the label when the field takes focus.
  • aria-required="true" when the field is required, worked out from required if you passed it and otherwise from the rules, counting a required rule only when it is not warningOnly.

What a control outside an Item does not get

All of the above comes from cloning. A control the Item cannot clone gets none of it, with no error, and each of these cases looks fine on screen.
  • An Item with no name derives no field id at all. Its label renders with no htmlFor, so it looks like a label and names nothing. That is fine for a submit row and wrong for a field.
  • An Item with more than one child has nothing single to clone. It warns in development and renders the children untouched, which means no id, no aria-describedby and no aria-invalid. Wrap the group in one element and put the named Items inside it, or use noStyle Items per control.
  • A noStyle Item registers the field but renders no label and no help block of its own. Its errors show on the nearest styled parent Item, so the message is not linked to this control through aria-describedby. Name the control yourself there.
  • A control that is not a Form field at all needs its own id and its own <label htmlFor>. Putting it inside a Form does nothing for it.

Required is more than an asterisk

The red asterisk is a visual convention with no accessible meaning of its own. What carries it is aria-required, and what carries it for a sighted reader who does not know the convention is the words around the field.
  • requiredMark defaults to true: required fields get the mark, optional ones get nothing. That reads well when most of the form is optional and badly when most of it is required, because the page fills with asterisks.
  • requiredMark="optional" inverts it: required fields get nothing and optional ones get a localized "optional" word beside the label. It is real text, so it is announced, which makes it the stronger of the two.
  • requiredMark={false} hides the mark and does not change aria-required. The field is still required to a screen reader and no longer marked for anybody else, so only reach for it when the surrounding copy says so instead.
  • requiredMark also takes a function, which renders the label however you like. Whatever you put there, keep it text rather than a bare glyph or a color.
  • Passing required on an Item marks it and sets aria-required without adding a rule, so the form still submits empty. Add a required rule to enforce it.

Announcing an error when it appears

The help block is linked through aria-describedby, and it is not a live region: it has no role="alert" and no aria-live. A description is read when the field takes focus, so a message that appears while focus is elsewhere is not announced at all.
  • The default validateTrigger hides this. Validation on onChange means the error appears while the reader is in the field, so the description is already there when they next move. Switch to onBlur and the message lands on a field they have just left.
  • Submit is the case that needs help. Several fields go invalid at once and focus is on the button. Nothing is announced. Either move focus to the first invalid field (below) or put a summary in your own live region.
  • Keep the message useful out of context. It is read straight after the label, so "Email: enter a valid address" works and "Invalid" does not.
  • help and the error share one slot. A static help node is replaced by the error message when one appears, so hint text written there disappears when the reader needs it most. Put a lasting hint in extra.

Moving focus to the first invalid field

scrollToFirstError scrolls. It does not focus, and the difference matters: a scroll moves the page for a sighted reader and leaves a keyboard or screen reader user where they were, on a submit button that appears to have done nothing.
  • Pass the options object with focus set: scrollToFirstError={{ focus: true, block: 'center' }}. The field is focused after the scroll, and only when that key is true.
  • block and behavior are passed straight through to the scroll, which is how you keep a sticky header from covering the field it just scrolled to.
  • Where you need more than the first field, onFinishFailed hands you every failing field, and form.scrollToField(name, { focus: true }) takes you to any of them.
  • Do not move focus on every keystroke's validation, only on submit. Focus that moves under a reader who did not ask for it is worse than a message they have to go and find.

Hint text, and the label tooltip

  • extra is the durable hint. It renders in its own block with the id <fieldId>_extra, and that id joins the control's aria-describedby next to the help block, so an error does not replace it.
  • Both ids only exist when the Item has a name. On an Item with no name there is no field id, so the extra block has no id and describes nothing.
  • The label's tooltip is not accessible on its own. Its trigger is a <span> with tabIndex={-1}, out of the tab order: a keyboard user cannot open it and a screen reader user is never told it is there. Anything a reader must have belongs in extra, and the tooltip is for the shortcut a mouse user gets on hover.
  • Keep hint text short and put it before the mistake rather than after. A format hint in extra is read with the field. The same sentence as an error message is read only once it is too late.

Disabled, contrast and motion

  • Disabled is not read-only. A disabled control is out of the tab order, so a keyboard reader cannot reach it to find out what it says, and it is not submitted. A value the reader should be able to read and copy but not change wants readOnly on the control, which stays focusable and stays in the form.
  • disabled on the Form disables every control inside it at once. That takes the whole form out of the tab order, which is fine for a saving state and wrong for a permanently read-only view.
  • Never let color alone say "error". The Item pairs the color with a message and with aria-invalid. A restyle that drops the message and keeps a red border removes the only part a color-blind reader can use.
  • Check the error ink, not the error border. The message sits on the form's own background, and a colorError chosen against white can fall under 4.5:1 in dark mode. The Tokens tab lists which tokens this component reads.
  • The error's appearance is animated. The help block slides in and out, which also shifts everything below it. For a reader who has asked for reduced motion, wrap the form in a ThemeScope whose light and dark configs both set token: { motion: false }, and put any motion you add behind motion-safe:.
  • Reserve the space a message will take where you can. A form whose fields jump down the page as errors appear is hard to use with a pointer and harder with a magnifier.