Persona

FormField

Inputs · 17 props · Updated Sep 22, 2026

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.

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.

  • label renders above the input.
  • helperText renders below it.
  • warningText and errorText render below the helper text, warning first.
  • rightActions renders to the right of the label, and is the slot most likely to be new to you.
  • labelProps reaches the label element, where tooltip turns 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.

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.

  • TextField wraps Input, the single-line text input.
  • TextAreaField wraps Text Area.
  • SelectField wraps Select, and reconciles the trigger’s id with the label’s.
  • ComboboxField wraps Combobox in both single and multi-select.
  • CheckboxField wraps one Checkbox, and adds checkboxLabel for the label beside the box.
  • CheckboxGroupField wraps a set of them, on React Aria’s checkbox group.
  • RadioGroupField wraps RadioGroup, the set of Radio buttons.
  • ToggleField wraps Toggle.
  • SegmentedControlField wraps SegmentedControl at full width by default.
  • ListBoxField wraps List Box, and rebuilds its footer buttons as a menu beside the label.
  • TableBoxField wraps TableBox, and is the only one that turns configField on 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.

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 labelOn and labelOff, 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 id from 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.

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.

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.

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.

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.

NameType
disabled

boolean

hasError

boolean

hasWarning

boolean

inputId

string

configField

boolean

Whether to render the form field as a config field. This will remove the label and helper text.

configFieldInputWidth

string

Override the width of the form input container when configField is true. Accepts any valid CSS width value (e.g. ‘33%’, ‘66.67%’). Defaults to ‘50%‘.

hasRightActions

boolean

Whether the form field has right actions.

label

ReactNode

Renders <Label> above input

helperText

ReactNode

Renders <HelperText> below input

errorText

ReactNode

Renders <ErrorText> below input and helper text.

warningText

ReactNode

Renders <WarningText> below input and helper text.

children

ReactElement<unknown, string | JSXElementConstructor<any>>

Should be an input of some sort

labelProps

FormLabelProps

helperProps

(HelperTextProps & RefAttributes<HTMLDivElement>)

warningProps

(Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, “ref”> & RefAttributes<HTMLDivElement>)

errorProps

(Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, “ref”> & RefAttributes<HTMLDivElement>)

rightActions

ReactNode

Renders actions to the right of the label.

Also accepts every prop of 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:

NameType
label

ReactNode

Renders <Label> above input

inputId

string

ID of the input element. Providing this will automatically link the label to the input.

helperText

ReactNode

Renders <HelperText> below input

errorText

ReactNode

Renders <ErrorText> below input and helper text.

warningText

ReactNode

Renders <WarningText> below input and helper text.

labelProps

FormLabelProps

helperProps

(HelperTextProps & RefAttributes<HTMLDivElement>)

warningProps

(Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, “ref”> & RefAttributes<HTMLDivElement>)

errorProps

(Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, “ref”> & RefAttributes<HTMLDivElement>)

configField

boolean

Whether to render the form field as a config field. This will render the helper text as a tooltip.

configFieldInputWidth

string

Override the width of the input area when configField is true. Accepts any valid CSS width value (e.g. ‘33%’, ‘66.67%’). Defaults to ‘50%‘.

rightActions

ReactNode

Renders actions to the right of the label.

size

InputSizes

Size of the input. If you need the HTML size attr, use inputSize.

inputSize

number

size HTML attr on <input>

leftIcon

ReactNode

hasRightItem

boolean

hasError

boolean

hasWarning

boolean

hovered

boolean

Visual state override. Forces the :hover styling on permanently, whatever the real state is. For specimens and visual tests, not application code.

focused

boolean

Visual state override. Forces the :focus styling on permanently, whatever the real state is. For specimens and visual tests, not application code.

active

boolean

Visual state override. Forces the :focus-within styling on permanently, whatever the real state is. For specimens and visual tests, not application code.

mixed

boolean

Visual state override. Forces the :indeterminate styling on permanently, whatever the real state is. For specimens and visual tests, not application code.

Also accepts every prop of 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:

NameType
children

ReactNode

label

ReactNode

Renders <Label> above input

inputId

string

ID of the input element. Providing this will automatically link the label to the input.

helperText

ReactNode

Renders <HelperText> below input

errorText

ReactNode

Renders <ErrorText> below input and helper text.

warningText

ReactNode

Renders <WarningText> below input and helper text.

labelProps

FormLabelProps

helperProps

(HelperTextProps & RefAttributes<HTMLDivElement>)

warningProps

(Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, “ref”> & RefAttributes<HTMLDivElement>)

errorProps

(Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, “ref”> & RefAttributes<HTMLDivElement>)

configField

boolean

Whether to render the form field as a config field. This will render the helper text as a tooltip.

configFieldInputWidth

string

Override the width of the input area when configField is true. Accepts any valid CSS width value (e.g. ‘33%’, ‘66.67%’). Defaults to ‘50%‘.

rightActions

ReactNode

Renders actions to the right of the label.

className

string

id

string

aria-label

string

Accessible label for the group

disabled

boolean

Whether the entire component is disabled

size

sm, md

Size of the segmented control

onSelectionChange

((key: Key) => void)

Callback when selection changes

selectedKey

Key

Current selected key (controlled)

defaultSelectedKey

Key

Default selected key (uncontrolled)

fullWidth

boolean

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}:

NameType
onAdd

((e: MouseEvent<HTMLButtonElement, MouseEvent>) => void)

onDelete

((selectedKey: string | null) => void)

deleteDisabled

boolean

actions

TableBoxFieldActionEntry[]

Actions and labeled action groups rendered in order. Empty groups are omitted.

addDropdownConfig

AddDropdownConfig

createNewRow

(() => string)

Callback to create a new row and add it to the rows array Should return the id of the newly created row

children

ReactNode

label

ReactNode

Renders <Label> above input

inputId

string

ID of the input element. Providing this will automatically link the label to the input.

helperText

ReactNode

Renders <HelperText> below input

errorText

ReactNode

Renders <ErrorText> below input and helper text.

warningText

ReactNode

Renders <WarningText> below input and helper text.

labelProps

FormLabelProps

helperProps

(HelperTextProps & RefAttributes<HTMLDivElement>)

warningProps

(Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, “ref”> & RefAttributes<HTMLDivElement>)

errorProps

(Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, “ref”> & RefAttributes<HTMLDivElement>)

configField

boolean

Whether to render the form field as a config field. This will render the helper text as a tooltip.

configFieldInputWidth

string

Override the width of the input area when configField is true. Accepts any valid CSS width value (e.g. ‘33%’, ‘66.67%’). Defaults to ‘50%‘.

rightActions

ReactNode

Renders actions to the right of the label.

Also accepts every prop of Omit<TableBoxProps<T>, ‘aria-label’ | ‘onCreateNewRow’>.

Was this page helpful?