Skip to main contentSkip to docs navigation

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.

We'll never share your email with anyone else.
JSX
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.

One code per order.
JSX
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.

Use 8 or more characters with a mix of letters, numbers and symbols.
JSX
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.

@
This username is already taken.
JSX
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 onSet on the control
inputthe label, as htmlForid
labelthe label, as its own idaria-labelledby
helpthe help text, as its idaria-describedby
feedbackthe feedback message, as its idaria-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>, needs input only.
  • A <TextInput> or <Textarea> takes both. input is enough to name it, but react-aria, which these are built on, reads only the control's own aria-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, takes label only: htmlFor does 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

PropTypeDefault
childrenReactNode—
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.