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
| Name | Type | Description |
|---|---|---|
FormField | Component | Root container; generates a unique id and provides context |
FormFieldLabel | Component | <label> with htmlFor injected from context |
FormFieldGroupLabel | Component | <span> label for group mode (RadioGroup, etc.); uses aria-labelledby instead of htmlFor |
FormFieldControl | Component | Wrapper that injects id, aria-describedby, and aria-invalid into its single child |
FormFieldDescription | Component | Helper text with stable id for aria-describedby |
FormFieldMessage | Component | Error 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
| Prop | Type | Component | Default | Description |
|---|---|---|---|---|
error | boolean | FormField | false | Activates error styles and role="alert" on the message |
group | boolean | FormField | false | Enables 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 Prop | Value | Purpose |
|---|---|---|
id | Auto-generated UUID | Associates control with label via htmlFor |
aria-describedby | "<id>-description <id>-message" | Links control to description and message |
aria-invalid | true (only when error=true) | Signals invalid state to screen readers |
Group mode (group={true})
| Injected Prop | Value | Purpose |
|---|---|---|
aria-labelledby | <id>-label | Associates control with FormFieldGroupLabel |
aria-describedby | "<id>-description <id>-message" | Links control to description and message |
aria-invalid | true (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
| State | Type | Values | Default | CSS Reflection |
|---|---|---|---|---|
error | boolean | — | — | BEM 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>receivesrole="group"andaria-labelledby FormFieldControlinjectsaria-labelledbyinstead ofidonto the childFormFieldGroupLabelrenders a<span>(not<label>) to avoid invalid HTML
Notes
FormFieldControlexpects exactly one child element. Wrapping multiple elements will throw.FormFieldMessagerendersnullwhenchildrenis empty, so it is safe to always include it.FormFieldLabelandFormFieldControlmust be used within<FormField>— they will throw if the context is missing.- The
errorprop onFormFieldpropagates to bothFormFieldLabel(error style) andFormFieldMessage(role="alert"). FormFieldLabelandFormFieldGroupLabelare mutually exclusive: usingFormFieldLabelin group mode (or vice versa) will throw.- For React Hook Form integration patterns, see the React Hook Form guide.