FormField wraps an input and renders the text around it. It owns the whole label contract: the visible label, the helper text, the validation messages, and the ids that tie them to the control.
Most of the time you will not write one. PDS ships eleven components that are a FormField and an input already composed, and they are the default for new code. This page is their index, and it is also the reference for the contract they are all built on, which is what you need when the control you have is one none of them wraps.
Anatomy
Section titled “Anatomy”The vertical order is fixed and nothing configures it: label, then the input, then helper text, then a warning, then an error. Every slot is optional, and a FormField holding only an input is legal and does nothing for you.
labelrenders above the input.helperTextrenders below it.warningTextanderrorTextrender below the helper text, warning first.rightActionsrenders to the right of the label, and is the slot most likely to be new to you.labelPropsreaches the label element, wheretooltipturns the label into a tooltip trigger with a dashed underline.
FormControl carries the same validation and layout props and is what FormField
is built on. The difference is how you hand it content: FormField takes the
label and the messages as props and renders the slots for you, where FormControl
takes them as children and leaves the composition to you.
When to use
Section titled “When to use”Eleven composed fields cover the inputs PDS ships
Section titled “Eleven composed fields cover the inputs PDS ships”Each one takes FormField’s props and the input’s props in one object and splits
them internally, so the label, the ids and the error state cannot be wired to
the wrong half. Twelve props go to the field — label, inputId,
helperText, errorText, warningText, the four *Props objects,
configField, configFieldInputWidth and rightActions — and everything else
goes to the control.
TextFieldwrapsInput, the single-line text input.TextAreaFieldwraps Text Area.SelectFieldwraps Select, and reconciles the trigger’s id with the label’s.ComboboxFieldwraps Combobox in both single and multi-select.CheckboxFieldwraps one Checkbox, and addscheckboxLabelfor the label beside the box.CheckboxGroupFieldwraps a set of them, on React Aria’s checkbox group.RadioGroupFieldwrapsRadioGroup, the set of Radio buttons.ToggleFieldwraps Toggle.SegmentedControlFieldwrapsSegmentedControlat full width by default.ListBoxFieldwraps List Box, and rebuilds its footer buttons as a menu beside the label.TableBoxFieldwrapsTableBox, and is the only one that turnsconfigFieldon by default.
Three of them do more than pass props through, which is why their own pages
carry the detail: CheckboxGroupField, ListBoxField and TableBoxField.
Five of them accept field props in their type and then discard them, because the
type is the union of both halves while the implementation forwards a subset by
hand. Count of the twelve reaching the field: RadioGroupField four,
CheckboxField six, SegmentedControlField six, ListBoxField and
TableBoxField ten each. In every case the leftovers are spread onto the
control, where no prop of that name exists, so they are dropped rather than
reinterpreted. Neither RadioGroupField nor CheckboxGroupField has a
configField at all, and ListBoxField and TableBoxField set rightActions
themselves, so yours is ignored.
Compose by hand for an input slot none of the eleven fits
Section titled “Compose by hand for an input slot none of the eleven fits”A hand-written FormField is for a field whose input slot holds something no composed field represents: two controls on one row, a control you have built yourself, a list with sibling markup beside it. That is the condition, and it is the only one.
It is not a rule about existing code. Manual composition is throughout the product and works; nothing here asks anyone to go and rewrite a call site that is doing its job.
Wrap any control that cannot name itself
Section titled “Wrap any control that cannot name itself”Almost none of them can, and they fail to in different ways. This is what the composed fields are doing on your behalf, and what you take on when you compose one by hand.
- Checkbox and Radio carry their own
label, which names the option beside the control. FormField supplies the label above the group, which is the question those options answer. - Select and Combobox have no label of their own, only
aria-label. FormField gives them the only visible label they have, and the id wiring that makes it their name. - Toggle carries
labelOnandlabelOff, which name the state rather than the field. The field still needs a name. - Slider, List Box and SwatchPicker do not read FormField’s context at all. They get a visible label out of it and not an accessible name, which has to be wired by hand.
- TextArea has neither, and reads its
idfrom FormField’s context. Without the wrapper there is nothing for a label to point at.
One input id is shared with everything inside the field
Section titled “One input id is shared with everything inside the field”FormField hands one input id to every control inside it, and that id wins over a
control’s own. Three checkboxes in one FormField therefore end up with three
identical ids, every label in the group points at the first checkbox, and
clicking the second label toggles the first. CheckboxGroupField clears the id
before rendering its children, which is why a set of checkboxes belongs in it;
Checkbox has the rest.
Even a single checkbox inside a labeled FormField ends up with two <label>
elements pointing at it, and a screen reader reads both as its name.
Validation
Section titled “Validation”Pass the message, not the boolean
Section titled “Pass the message, not the boolean”errorText sets hasError for you, and warningText sets hasWarning. The
booleans exist for the case where the message lives somewhere else on the page,
and setting one by hand overrides what the text implied, including back to
false while the message is still on screen.
Read a field showing both messages as one error
Section titled “Read a field showing both messages as one error”hasError and hasWarning are independent booleans, and both can be true. When
they are, both messages render, stacked with the warning above the error, and
every color resolves in the error’s favor: anything that colors itself from the
pair checks error first and never reaches the warning branch. So the field
borders red, both lines of text come out red, and the warning reads as a second
sentence of the error.
The warning tier is a full step between helper text and error, and it is the part of this component people are most often surprised to find.
Config fields
Section titled “Config fields”A config field drops the warning and the error text
Section titled “A config field drops the warning and the error text”configField drops the warning and the error text, which do not render at all,
and turns the helper text into a tooltip on the label rather than a line under
the field. The label itself stays, and moves to the left of the input, which
takes half the width until configFieldInputWidth says otherwise.
A textarea is the exception. A config field containing one drops back to the stacked layout, label above field, at full width.
The props table below says configField removes the label and the helper text.
That description belongs to FormControl’s copy of the prop and does not
describe what FormField renders. The behavior above is what the component does.
Helper text in a config field is easy to miss
Section titled “Helper text in a config field is easy to miss”Helper text in a config field is only visible to a reader who goes looking for it on the label. A constraint they need while filling the field in belongs in the label or in the option text, where it stays.
Accessibility
Section titled “Accessibility”Pass an inputId when anything has to point at the field
Section titled “Pass an inputId when anything has to point at the field”FormField generates an id for the input and derives the rest from it, adding
-label, -help-text, -warning-text and -error-text. All four go to the
control through context, and each control decides which of them to reference.
Generated, those ids are unpredictable. Pass inputId and they become yours,
which is the only way to write an aria-labelledby or an aria-describedby
that survives a re-render. A Slider needs exactly this:
it does not read the context at all, so its FormField label is visible without
being its accessible name until you point aria-labelledby at the label’s id.
Wire the error message yourself
Section titled “Wire the error message yourself”Nothing in FormField associates errorText with the control.
Select points aria-errormessage at the helper text’s id,
Combobox lists the helper and warning ids and omits the
error, and TextArea sets only aria-invalid. In all
three, a field whose only message is an error shows it and does not announce it.
Until that is fixed upstream, either put the error’s substance in helper text as
well, or pass inputId and point the control’s own aria-describedby at that
id with -error-text appended.
CheckboxGroupField is the exception, and it is the only one. React Aria
generates the ids for its helper and error text and lists both in the group’s
aria-describedby, so a checkbox group given errorText announces it. Nothing
else in the set does.
rightActions has a smaller version of the same problem. It nests the label
inside a layout element, where FormField’s scan of its own children no longer
finds it, so no label id is registered and anything that needs one has to be
given it by hand.
FormField’s own props. FormControl’s half of the API is in here too, which is
why the table is longer than the twelve props the composed fields split out.
| Name | Type |
|---|---|
disabled |
|
hasError |
|
hasWarning |
|
inputId |
|
configField |
Whether to render the form field as a config field. This will remove the label and helper text. |
configFieldInputWidth |
Override the width of the form input container when |
hasRightActions |
Whether the form field has right actions. |
label |
Renders |
helperText |
Renders |
errorText |
Renders |
warningText |
Renders |
children |
Should be an input of some sort |
labelProps |
|
helperProps |
|
warningProps |
|
errorProps |
|
rightActions |
Renders actions to the right of the label. |
ComponentPropsWithoutRef<‘div’>.Three of the eleven have no page of their own
Section titled “Three of the eleven have no page of their own”Input, SegmentedControl and TableBox are not documented here yet, so their
field components’ tables live on this page. The other eight are on the page of
the control they wrap.
TextField, which wraps Input:
| Name | Type |
|---|---|
label |
Renders |
inputId |
ID of the input element. Providing this will automatically link the label to the input. |
helperText |
Renders |
errorText |
Renders |
warningText |
Renders |
labelProps |
|
helperProps |
|
warningProps |
|
errorProps |
|
configField |
Whether to render the form field as a config field. This will render the helper text as a tooltip. |
configFieldInputWidth |
Override the width of the input area when |
rightActions |
Renders actions to the right of the label. |
size |
Size of the input. If you need the HTML size attr, use inputSize. |
inputSize |
size |
leftIcon |
|
hasRightItem |
|
hasError |
|
hasWarning |
|
hovered |
Visual state override. Forces the |
focused |
Visual state override. Forces the |
active |
Visual state override. Forces the |
mixed |
Visual state override. Forces the |
Omit<ComponentPropsWithoutRef<‘input’>, ‘size’>.SegmentedControlField, which sets fullWidth on the control before your props
are applied, so the field is full width unless you pass fullWidth={false}
yourself:
| Name | Type |
|---|---|
children |
|
label |
Renders |
inputId |
ID of the input element. Providing this will automatically link the label to the input. |
helperText |
Renders |
errorText |
Renders |
warningText |
Renders |
labelProps |
|
helperProps |
|
warningProps |
|
errorProps |
|
configField |
Whether to render the form field as a config field. This will render the helper text as a tooltip. |
configFieldInputWidth |
Override the width of the input area when |
rightActions |
Renders actions to the right of the label. |
className |
|
id |
|
aria-label |
Accessible label for the group |
disabled |
Whether the entire component is disabled |
size |
Size of the segmented control |
onSelectionChange |
Callback when selection changes |
selectedKey |
Current selected key (controlled) |
defaultSelectedKey |
Default selected key (uncontrolled) |
fullWidth |
Whether the component should take full width |
TableBoxField, the only composed field with configField on by default, which
means its Add button and its actions menu sit beside the label unless you pass
configField={false}:
| Name | Type |
|---|---|
onAdd |
|
onDelete |
|
deleteDisabled |
|
actions |
Actions and labeled action groups rendered in order. Empty groups are omitted. |
addDropdownConfig |
|
createNewRow |
Callback to create a new row and add it to the rows array Should return the id of the newly created row |
children |
|
label |
Renders |
inputId |
ID of the input element. Providing this will automatically link the label to the input. |
helperText |
Renders |
errorText |
Renders |
warningText |
Renders |
labelProps |
|
helperProps |
|
warningProps |
|
errorProps |
|
configField |
Whether to render the form field as a config field. This will render the helper text as a tooltip. |
configFieldInputWidth |
Override the width of the input area when |
rightActions |
Renders actions to the right of the label. |
Omit<TableBoxProps<T>, ‘aria-label’ | ‘onCreateNewRow’>.Was this page helpful?