Field

A form field wrapper that composes a label, input, description, and error message.

Spec · from metadata

When to use

  • •Wrapping any form control (Input, Select, Textarea, etc.) with a label
  • •Displaying field-level validation errors
  • •Building form layouts with consistent spacing and alignment
  • •Grouping related fields with FieldSet, FieldGroup, and FieldLegend

When not to use

  • •Standalone labels without a form control — use Label directly
  • •Page-level error messages — use Alert or toast instead
  • •Non-form layout — use standard flex/grid utilities

Variants

PropValuesDefaultDescription
orientationverticalhorizontalresponsiveverticalvertical stacks label above input. horizontal places them side by side. responsive switches at @md container breakpoint.
variant (FieldLegend)legendlabellegendControls font size: legend is text-base, label is text-sm.

Anti-patterns

Avoid<label>Email</label>
<Input id="email" />
Prefer<Field>
  <FieldLabel htmlFor="email">Email</FieldLabel>
  <Input id="email" />
</Field>

Use Field + FieldLabel for consistent spacing, error handling, and styling.

Avoid<Field orientation="horizontal">
  <FieldLabel>Name</FieldLabel>
  <Input />
  <FieldDescription>Your full name</FieldDescription>
  <FieldError>Required</FieldError>
</Field>
Prefer<Field orientation="horizontal">
  <FieldLabel>Name</FieldLabel>
  <FieldContent>
    <Input />
    <FieldDescription>Your full name</FieldDescription>
    <FieldError>Required</FieldError>
  </FieldContent>
</Field>

In horizontal orientation, wrap input + description + error in FieldContent for proper layout.

Avoid<Field>
  <FieldLabel htmlFor="middle-name">Middle name (optional)</FieldLabel>
  <Input id="middle-name" />
</Field>
Prefer<RequiredFieldsNote variant="marked" />
<Field required>
  <FieldLabel htmlFor="last-name">Last name</FieldLabel>
  <Input id="last-name" required />
</Field>
<Field>
  <FieldLabel htmlFor="middle-name">Middle name</FieldLabel>
  <Input id="middle-name" />
</Field>

Mark the required fields, not the optional ones. Whatever an UNMARKED label means is the convention's real payload: with "(optional)" labels an unmarked field is required, with asterisks an unmarked field is optional. Mix the two and an unmarked field means nothing, so the reader has to read every label before trusting any of them — two conventions here are not merely inconsistent, each is individually useless once the other is present. `required` on Field renders the aria-hidden asterisk via FieldLabel; because Field wraps arbitrary children it cannot reach the control, so pass `required` to the control too and a dev warning (Field.required-without-aria) fires if you forget. On the react-hook-form path, `required` on FormItem does wire both halves by itself. See content/decisions/required-fields.md.

Accessibility

  • Required ARIAdata-invalid on Field for error styling
  • Screen readerFieldError renders with role='alert' for live region announcements. FieldSet uses semantic <fieldset> with <legend>.
  • ContrastError text uses text-destructive. Description uses text-muted-foreground.

Token bindings

TokenCategoryUsage
destructivecolorError text and invalid state styling
muted-foregroundcolorDescription text
primarycolorChecked state highlight on FieldLabel with checkbox/radio
bordercolorFieldSeparator line

Import

import {
  Field,
  FieldContent,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLabel,
  FieldLegend,
  FieldSeparator,
  FieldSet,
  FieldTitle,
  RequiredFieldsNote,
} from "@timelycare/helix-ui"

Styling

Typography

ElementFontSizeWeightColor
LabelAdelle Sans Medium14px (text-sm)font-mediumtext-foreground
LegendAdelle Sans Semibold16px (text-base)font-semiboldtext-foreground
Legend (label variant)Adelle Sans Medium14px (text-sm)font-mediumtext-foreground
TitleAdelle Sans Medium14px (text-sm)font-mediumtext-foreground
DescriptionAdelle Sans Regular14px (text-sm)font-normaltext-muted-foreground
ErrorAdelle Sans Regular14px (text-sm)font-normaltext-destructive

Spacing

PropertyValueTailwind
Field gap (vertical)8pxgap-2
FieldGroup gap16pxgap-4
FieldSeparator1px borderborder-t border-border
Label to input8pxHandled by Field's gap-2

Orientation

OrientationBehavior
vertical (default)Label stacks above input
horizontalLabel and input sit side-by-side
responsiveVertical on mobile, horizontal on desktop (container-based)

Sub-Components

ComponentPurpose
FieldIndividual field wrapper (label + control + description/error)
FieldLabelForm label with htmlFor association
FieldDescriptionHelper text below or near the control
FieldErrorValidation error message in text-destructive. When passed an errors array, duplicate messages are automatically deduplicated before rendering.
FieldGroupGroups multiple fields with consistent spacing
FieldSetSemantic <fieldset> wrapper for related fields
FieldLegend<legend> for a FieldSet with heading styling
FieldContentWrapper for label + description when using horizontal/responsive layout
FieldTitleTitle text within a selectable field (e.g., radio card)
FieldSeparatorVisual 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 required marks the label with an aria-hidden asterisk and sets aria-required on the control
  • FieldLabel renders a <label> with htmlFor association to the input
  • FieldSet renders a semantic <fieldset> element
  • FieldLegend renders a <legend> for the fieldset
  • FieldDescription should be linked via aria-describedby on the input
  • FieldError should be linked via aria-describedby and the input should have aria-invalid="true"
  • Keyboard: Tab moves between fields; Space/Enter activates checkboxes and radios

Gotchas

ProblemSolution
Label not linked to inputEnsure htmlFor on FieldLabel matches the input id
Error message not announcedAdd aria-describedby pointing to the error element
Responsive layout not switchingUse orientation="responsive" — requires container query support
Checkbox label too boldAdd className="font-normal" to FieldLabel for checkbox/radio fields
Description above inputPlace FieldDescription before or after the input — order in JSX determines visual order
Multi-column fields misalignedWrap in a div with grid grid-cols-2 gap-4 inside FieldGroup
Asterisks show but nothing explains themAdd <RequiredFieldsNote /> above the fields — the note is what makes the marker mean something
Every label has an asteriskMark none and use <RequiredFieldsNote variant="all" /> instead

See Also


Last updated: September 9, 2026