Import
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
FieldTitle,
RequiredFieldsNote,
} from "@timelycare/helix-ui"
Styling
Typography
| Element | Font | Size | Weight | Color |
|---|---|---|---|---|
| Label | Adelle Sans Medium | 14px (text-sm) | font-medium | text-foreground |
| Legend | Adelle Sans Semibold | 16px (text-base) | font-semibold | text-foreground |
| Legend (label variant) | Adelle Sans Medium | 14px (text-sm) | font-medium | text-foreground |
| Title | Adelle Sans Medium | 14px (text-sm) | font-medium | text-foreground |
| Description | Adelle Sans Regular | 14px (text-sm) | font-normal | text-muted-foreground |
| Error | Adelle Sans Regular | 14px (text-sm) | font-normal | text-destructive |
Spacing
| Property | Value | Tailwind |
|---|---|---|
| Field gap (vertical) | 8px | gap-2 |
| FieldGroup gap | 16px | gap-4 |
| FieldSeparator | 1px border | border-t border-border |
| Label to input | 8px | Handled by Field's gap-2 |
Orientation
| Orientation | Behavior |
|---|---|
vertical (default) | Label stacks above input |
horizontal | Label and input sit side-by-side |
responsive | Vertical on mobile, horizontal on desktop (container-based) |
Sub-Components
| Component | Purpose |
|---|---|
Field | Individual field wrapper (label + control + description/error) |
FieldLabel | Form label with htmlFor association |
FieldDescription | Helper text below or near the control |
FieldError | Validation error message in text-destructive. When passed an errors array, duplicate messages are automatically deduplicated before rendering. |
FieldGroup | Groups multiple fields with consistent spacing |
FieldSet | Semantic <fieldset> wrapper for related fields |
FieldLegend | <legend> for a FieldSet with heading styling |
FieldContent | Wrapper for label + description when using horizontal/responsive layout |
FieldTitle | Title text within a selectable field (e.g., radio card) |
FieldSeparator | Visual divider between field sections |
Common Patterns
Basic Input Field
<Field>
<FieldLabel htmlFor="username">Username</FieldLabel>
<Input id="username" placeholder="Enter username" />
<FieldDescription>Choose a unique username for your account.</FieldDescription>
</Field>
With Validation Error
<Field>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input
id="email"
placeholder="m@example.com"
className="border-destructive focus-visible:ring-destructive/50"
/>
<FieldError>Please enter a valid email address.</FieldError>
</Field>
Grouped Fields with FieldSet
<FieldSet>
<FieldLegend>Address Information</FieldLegend>
<FieldDescription>We need your address to deliver your order.</FieldDescription>
<FieldGroup>
<Field>
<FieldLabel htmlFor="street">Street Address</FieldLabel>
<Input id="street" placeholder="123 Main St" />
</Field>
<div className="grid grid-cols-2 gap-4">
<Field>
<FieldLabel htmlFor="city">City</FieldLabel>
<Input id="city" placeholder="New York" />
</Field>
<Field>
<FieldLabel htmlFor="zip">Postal Code</FieldLabel>
<Input id="zip" placeholder="90502" />
</Field>
</div>
</FieldGroup>
</FieldSet>
Horizontal Checkbox
<Field orientation="horizontal">
<Checkbox id="terms" />
<FieldLabel htmlFor="terms" className="font-normal">
I agree to the terms and conditions
</FieldLabel>
</Field>
Checkbox Group with FieldSet
<FieldSet>
<FieldLegend variant="label">Notification Preferences</FieldLegend>
<FieldDescription>Select how you'd like to be notified.</FieldDescription>
<FieldGroup className="gap-3">
<Field orientation="horizontal">
<Checkbox id="email-notif" />
<FieldLabel htmlFor="email-notif" className="font-normal">Email</FieldLabel>
</Field>
<Field orientation="horizontal">
<Checkbox id="sms-notif" />
<FieldLabel htmlFor="sms-notif" className="font-normal">SMS</FieldLabel>
</Field>
</FieldGroup>
</FieldSet>
Responsive Layout
<FieldSet>
<FieldLegend>Profile</FieldLegend>
<FieldDescription>Fill in your profile information.</FieldDescription>
<FieldSeparator />
<FieldGroup>
<Field orientation="responsive">
<FieldContent>
<FieldLabel htmlFor="name">Name</FieldLabel>
<FieldDescription>Provide your full name</FieldDescription>
</FieldContent>
<Input id="name" placeholder="Jane Doe" required />
</Field>
<FieldSeparator />
<Field orientation="responsive">
<FieldContent>
<FieldLabel htmlFor="bio">Bio</FieldLabel>
<FieldDescription>Keep it under 100 characters.</FieldDescription>
</FieldContent>
<Textarea id="bio" placeholder="Tell us about yourself" className="resize-none" />
</Field>
</FieldGroup>
</FieldSet>
Required Field
<RequiredFieldsNote variant="marked" />
<Field required>
<FieldLabel htmlFor="last-name">Last name</FieldLabel>
<Input id="last-name" />
</Field>
<Field>
<FieldLabel htmlFor="middle-name">Middle name</FieldLabel>
<Input id="middle-name" />
</Field>
Selectable Radio Card
<FieldSet>
<FieldLabel htmlFor="plan">Choose a Plan</FieldLabel>
<RadioGroup defaultValue="basic">
<FieldLabel htmlFor="basic">
<Field orientation="horizontal" className="rounded-lg bg-input-bg">
<FieldContent>
<FieldTitle>Basic</FieldTitle>
<FieldDescription>Great for getting started.</FieldDescription>
</FieldContent>
<RadioGroupItem value="basic" id="basic" />
</Field>
</FieldLabel>
<FieldLabel htmlFor="pro">
<Field orientation="horizontal" className="rounded-lg bg-input-bg">
<FieldContent>
<FieldTitle>Pro</FieldTitle>
<FieldDescription>For power users and teams.</FieldDescription>
</FieldContent>
<RadioGroupItem value="pro" id="pro" />
</Field>
</FieldLabel>
</RadioGroup>
</FieldSet>
Required Fields
required on Field renders an aria-hidden asterisk through FieldLabel and sets
aria-required on the control. Both halves come from the one prop, so the visible marker and
the announced state cannot drift apart.
Pair it with <RequiredFieldsNote /> above the FieldGroup — variant="marked" for the
asterisk legend, variant="all" when every field is required and none is marked. The note is
self-hiding, so it renders nothing on a form that turns out to have no required fields.
Anti-pattern: a label reading Middle name (optional). Marking the optional fields instead
of the required ones is the rejected convention, and mixing the two leaves an unmarked label
meaning nothing at all. It logs a dev warning.
See Decision: Marking Required Fields for the reasoning and the all-required refinement.
Accessibility
Field requiredmarks the label with anaria-hiddenasterisk and setsaria-requiredon the controlFieldLabelrenders a<label>withhtmlForassociation to the inputFieldSetrenders a semantic<fieldset>elementFieldLegendrenders a<legend>for the fieldsetFieldDescriptionshould be linked viaaria-describedbyon the inputFieldErrorshould be linked viaaria-describedbyand the input should havearia-invalid="true"- Keyboard: Tab moves between fields; Space/Enter activates checkboxes and radios
Gotchas
| Problem | Solution |
|---|---|
| Label not linked to input | Ensure htmlFor on FieldLabel matches the input id |
| Error message not announced | Add aria-describedby pointing to the error element |
| Responsive layout not switching | Use orientation="responsive" — requires container query support |
| Checkbox label too bold | Add className="font-normal" to FieldLabel for checkbox/radio fields |
| Description above input | Place FieldDescription before or after the input — order in JSX determines visual order |
| Multi-column fields misaligned | Wrap in a div with grid grid-cols-2 gap-4 inside FieldGroup |
| Asterisks show but nothing explains them | Add <RequiredFieldsNote /> above the fields — the note is what makes the marker mean something |
| Every label has an asterisk | Mark none and use <RequiredFieldsNote variant="all" /> instead |
See Also
- Related Components: Form (React Hook Form integration), Input, Checkbox, Radio Group, Select, Label
- Decisions: Marking Required Fields — the asterisk convention and why "(optional)" labels are rejected
- Tokens: Colors — Destructive color for errors, muted-foreground for descriptions
Last updated: September 9, 2026