Form Field
A universal layout wrapper that handles label, help text, and validation feedback placement for any form control.
Overview
<FormField> wraps a form control (or a small group of them) with a .form-field grid layout, along with an associated FormLabel, FormHelp, and FormFeedback — see chassis-css's Form Field docs for the underlying CSS.
Every field component (TextInput, Textarea, Select, RangeInput, FileInput, ColorInput, Combobox, Autocomplete, DatePicker, DateRangePicker, TimeField, NumberField, SearchField, ChipInput, OtpInput) already accepts label/help/invalidFeedback/validFeedback props directly, lays itself out the same way and wires the label, help and feedback to its control — reach for those first, and don't wrap one of them in <FormField> to label it. Use <FormField> for what they can't do: a control with no field props of its own, like an <InputGroup>, or more than one element — like an input alongside a PasswordStrength meter — under a single label and help text.
<FormField> renders nothing but its children bare when none of label/help/validFeedback/invalidFeedback are set, so it's always safe to reach for.
Examples
Own field props
A field component that already accepts label/help needs no <FormField> wrapper at all, and no ids: it names its control and describes it with the help text itself.
import { TextInput } from '@chassis-ui/react'
export const Example = () => {
return (
<TextInput
label="Email address"
help="We'll never share your email with anyone else."
id="ffEmail"
/>
)
}Wrapping a control
A control with no label/help props of its own — like <InputGroup> — still gets the same layout wrapped in <FormField>. The <TextInput> inside the group can't take label itself, which would put the label and help inside the group, so it names the label and the help that <FormField> renders through ids — see Manual id wiring.
import { Button, FormField, InputGroup, TextInput } from '@chassis-ui/react'
export const Example = () => {
return (
<FormField
label="Coupon code"
help="One code per order."
ids={{ help: 'ffCouponHelp', input: 'ffCoupon', label: 'ffCouponLabel' }}
>
<InputGroup>
<TextInput
aria-describedby="ffCouponHelp"
aria-labelledby="ffCouponLabel"
id="ffCoupon"
name="coupon"
/>
<Button color="secondary" type="button" variant="outline">
Apply
</Button>
</InputGroup>
</FormField>
)
}Companion content
Group a control with related content, like a <PasswordStrength> meter, under one label. This is the one case for a <TextInput> inside <FormField>: its own label and help would wrap the input alone and leave the meter outside the field.
import { useState } from 'react'
import { FormField, PasswordStrength, TextInput } from '@chassis-ui/react'
export const Example = () => {
const [password, setPassword] = useState('')
return (
<FormField
label="Password"
help="Use 8 or more characters with a mix of letters, numbers and symbols."
ids={{ help: 'ffPasswordHelp', input: 'ffPassword', label: 'ffPasswordLabel' }}
>
<TextInput
aria-describedby="ffPasswordHelp"
aria-labelledby="ffPasswordLabel"
autoComplete="new-password"
id="ffPassword"
onChange={setPassword}
type="password"
value={password}
/>
<PasswordStrength value={password} />
</FormField>
)
}Validation feedback
invalidFeedback/validFeedback render only alongside a truthy invalid/valid, matching the leaf components' own validation props. <FormField> never changes its children, so set invalid/valid on the control too: its state is what shows the feedback and draws its border. Point the control's aria-describedby at ids.feedback so the message is read with it.
import { FormField, InputGroup, InputGroupAddon, TextInput } from '@chassis-ui/react'
export const Example = () => {
return (
<FormField
label="Username"
invalid
invalidFeedback="This username is already taken."
ids={{ feedback: 'ffUsernameFeedback', input: 'ffUsername', label: 'ffUsernameLabel' }}
>
<InputGroup>
<InputGroupAddon>@</InputGroupAddon>
<TextInput
aria-describedby="ffUsernameFeedback"
aria-labelledby="ffUsernameLabel"
defaultValue="chassis"
id="ffUsername"
invalid
name="username"
/>
</InputGroup>
</FormField>
)
}Manual id wiring
<FormField> doesn't clone its children, so it can't wire ids onto a control it doesn't own. Without the wiring the label and help are only text beside the control: a screen reader announces neither with it. Choose the ids, pass them through the ids prop, and repeat them on the control:
ids key | <FormField> puts it on | Set on the control |
|---|---|---|
input | the label, as htmlFor | id |
label | the label, as its own id | aria-labelledby |
help | the help text, as its id | aria-describedby |
feedback | the feedback message, as its id | aria-describedby, while the state is set |
Which of input and label to use depends on the control:
- A native
<input>,<select>or<textarea>, or a<Select>, needsinputonly. - A
<TextInput>or<Textarea>takes both.inputis enough to name it, but react-aria, which these are built on, reads only the control's ownaria-label/aria-labelledby, and warns in development about a field it finds neither on. - A group with no single input to target, like a
role="group"element, takeslabelonly:htmlFordoes nothing on an element that isn't a form control.
A control's own aria-label wins over both. <ChipInput> falls back to one ("Add value") unless it gets label, aria-label or aria-labelledby, which is one more reason to pass label to it rather than wrap it.
API
FormField
| Prop | Type | Default |
|---|---|---|
children | ReactNode | — |
| The form control(s) this field wraps. | ||
className? | string | — |
| A string of all className you want applied to the component. | ||
help? | ReactNode | — |
| A description for the field, rendered below the control(s). | ||
ids? | FormFieldIds | — |
The DOM ids of the wrapped control(s), used to associate the label (htmlFor) and
point your control's own aria-describedby at the rendered help/feedback text. | ||
invalid? | boolean | — |
| Set field validation state to invalid. | ||
invalidFeedback? | ReactNode | — |
An error message for the field, rendered below the control(s) when invalid is set. | ||
label? | ReactNode | — |
The field's caption, rendered as a FormLabel associated with ids.input. | ||
valid? | boolean | — |
| Set field validation state to valid. | ||
validFeedback? | ReactNode | — |
A success message for the field, rendered below the control(s) when valid is set. | ||