Components

FormField

Composite form field providing accessible label, control, description, and error message wiring via context.

Category: Form / Composite

Preview

Loading preview...
import {
  FormField,
  FormFieldLabel,
  FormFieldControl,
  FormFieldDescription,
  FormFieldMessage,
  Input,
} from "@hareru/ui"

<FormField>
  <FormFieldLabel>Email</FormFieldLabel>
  <FormFieldControl>
    <Input type="email" placeholder="you@example.com" />
  </FormFieldControl>
  <FormFieldDescription>The email address used for login</FormFieldDescription>
</FormField>

Import

import {
  FormField,
  FormFieldLabel,
  FormFieldGroupLabel,
  FormFieldControl,
  FormFieldDescription,
  FormFieldMessage,
} from "@hareru/ui"

Exports

NameTypeDescription
FormFieldComponentRoot container; generates a unique id and provides context
FormFieldLabelComponent<label> with htmlFor injected from context
FormFieldGroupLabelComponent<span> label for group mode (RadioGroup, etc.); uses aria-labelledby instead of htmlFor
FormFieldControlComponentWrapper that injects id, aria-describedby, and aria-invalid into its single child
FormFieldDescriptionComponentHelper text with stable id for aria-describedby
FormFieldMessageComponentError or validation message; renders as role="alert" when in error state

Types

export interface FormFieldProps extends React.HTMLAttributes<HTMLDivElement> {
  error?: boolean
  group?: boolean
}

export interface FormFieldLabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {}
export interface FormFieldGroupLabelProps extends React.HTMLAttributes<HTMLSpanElement> {}
export interface FormFieldControlProps extends React.HTMLAttributes<HTMLDivElement> {}
export interface FormFieldDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {}
export interface FormFieldMessageProps extends React.HTMLAttributes<HTMLParagraphElement> {}

Key Props

PropTypeComponentDefaultDescription
errorbooleanFormFieldfalseActivates error styles and role="alert" on the message
groupbooleanFormFieldfalseEnables group mode: uses role="group" + aria-labelledby instead of htmlFor

Accessibility Wiring

FormFieldControl automatically injects the following props into its direct child element:

Default mode (group={false})

Injected PropValuePurpose
idAuto-generated UUIDAssociates control with label via htmlFor
aria-describedby"<id>-description <id>-message"Links control to description and message
aria-invalidtrue (only when error=true)Signals invalid state to screen readers

Group mode (group={true})

Injected PropValuePurpose
aria-labelledby<id>-labelAssociates control with FormFieldGroupLabel
aria-describedby"<id>-description <id>-message"Links control to description and message
aria-invalidtrue (only when error=true)Signals invalid state to screen readers

Note: In group mode, id is not injected (the child manages its own id).

Structure

  • FormFieldLabel [label]
  • FormFieldGroupLabel [label]
  • FormFieldControl [control] (expected)
  • FormFieldDescription [description]
  • FormFieldMessage [description]

(expected) = recommended in canonical composition, not runtime-required.

States

StateTypeValuesDefaultCSS Reflection
errorbooleanBEM modifier

Accessibility

  • ARIA attributes: aria-invalid, aria-describedby, aria-labelledby
  • Notes: Automatically links label, input, and error message via aria-describedby. Group mode uses role='group' with aria-labelledby. FormFieldMessage renders with role='alert' when error is true.

Usage

import {
  FormField,
  FormFieldLabel,
  FormFieldControl,
  FormFieldDescription,
  FormFieldMessage,
  Input,
  Textarea,
  Button,
} from "@hareru/ui"

// Basic form field
<FormField>
  <FormFieldLabel>Email</FormFieldLabel>
  <FormFieldControl>
    <Input type="email" placeholder="you@example.com" />
  </FormFieldControl>
  <FormFieldDescription>The email address used for login</FormFieldDescription>
</FormField>

// With validation error
<FormField error={!!errors.name}>
  <FormFieldLabel>Name</FormFieldLabel>
  <FormFieldControl>
    <Input placeholder="John Doe" />
  </FormFieldControl>
  <FormFieldDescription>Used as your display name</FormFieldDescription>
  <FormFieldMessage>Name is required</FormFieldMessage>
</FormField>

// With Textarea
<FormField error={!!errors.bio}>
  <FormFieldLabel>Bio</FormFieldLabel>
  <FormFieldControl>
    <Textarea rows={4} placeholder="Tell us about yourself..." />
  </FormFieldControl>
  <FormFieldMessage>{errors.bio?.message}</FormFieldMessage>
</FormField>

Group Mode

Use group prop with FormFieldGroupLabel for controls that cannot be associated via <label htmlFor> (e.g., RadioGroup):

import {
  FormField,
  FormFieldGroupLabel,
  FormFieldControl,
  FormFieldMessage,
  RadioGroup,
  RadioGroupItem,
} from "@hareru/ui"

<FormField group error={!!errors.fruit}>
  <FormFieldGroupLabel>Preferred fruit</FormFieldGroupLabel>
  <FormFieldControl>
    <RadioGroup value={fruit} onValueChange={setFruit}>
      <RadioGroupItem value="apple">Apple</RadioGroupItem>
      <RadioGroupItem value="banana">Banana</RadioGroupItem>
    </RadioGroup>
  </FormFieldControl>
  <FormFieldMessage>{errors.fruit?.message}</FormFieldMessage>
</FormField>

In group mode:

  • The root <div> receives role="group" and aria-labelledby
  • FormFieldControl injects aria-labelledby instead of id onto the child
  • FormFieldGroupLabel renders a <span> (not <label>) to avoid invalid HTML

Notes

  • FormFieldControl expects exactly one child element. Wrapping multiple elements will throw.
  • FormFieldMessage renders null when children is empty, so it is safe to always include it.
  • FormFieldLabel and FormFieldControl must be used within <FormField> — they will throw if the context is missing.
  • The error prop on FormField propagates to both FormFieldLabel (error style) and FormFieldMessage (role="alert").
  • FormFieldLabel and FormFieldGroupLabel are mutually exclusive: using FormFieldLabel in group mode (or vice versa) will throw.
  • For React Hook Form integration patterns, see the React Hook Form guide.

On this page