Field
Pairs a control with a label and optional description, as a row or a card.
@garn/ui/fieldOn this page
Default
Settings row: label + description + switch.
Card
Card appearance.
Controls
Same Field with a checkbox and radio items.
Installation
garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.
npx garn-ui add field- Registry
field- Deps
class-variance-authority- Registry deps
utilslabel
Import
import { Field } from "@garn/ui/field";Props
The component's public props and their types.
appearance- Description
- No description.
- Type
"card" | "plain"- Default
plain
orientation- Description
- No description.
- Type
"end" | "start"- Default
start
childrenRequired- Description
- The control: a Switch, Checkbox, or single RadioGroupItem.
- Type
React.ReactElement<unknown, string | React.JSXElementConstructor<any>>
description- Description
- Optional one-line explanation, wired to the control via `aria-describedby`.
- Type
React.ReactNode
labelRequired- Description
- The setting name — becomes the control's accessible name via `htmlFor`.
- Type
React.ReactNode
size- Description
- Label text size — pair with the control's own `size`.
- Type
"lg" | "md" | "sm"
Plus 279 inherited native <div> attributes.
Styling
Target these data-slots and states, and remap these tokens, to restyle without forking the component.
[data-slot="field"]States
| Selector | State | Description |
|---|---|---|
[data-appearance=card]:has([data-state=checked]) | checked | A card flags the contained control's checked state — its edge goes --garn-brand-solid (a selected-option affordance). |
| appearance | `plain` row vs `card` surface. |
| orientation | `start` (control-leading) vs `end` (text-leading, space-between). |
| Property | Token | Tier |
|---|---|---|
cardSurface | --garn-card | semantic |
cardPadding | --garn-pad-panel | semantic · air |
selectedEdge | --garn-brand-solidbrandable | semantic |
descriptionColor | --garn-muted-foreground | semantic |
When to use
Pair a Switch / Checkbox / RadioGroupItem with a label (+ optional description), as a plain row or a selectable card.
Reach for something else when
- A form bound to react-hook-form with validation messages (use Form)
- a label with no control (use Label directly).
Overview
Field is the shared control-field: it takes a Switch, Checkbox, or a single RadioGroupItem as its child and lays it beside a label and an optional one-line description, doing the accessible-name (htmlFor) and description (aria-describedby) wiring for you. appearance="card" turns the row into a bordered option card that highlights when its control is checked, and orientation flips between a control-leading row (checkboxes, radios) and a text-leading settings row (the switch on the right). Because it wires by cloning the control, one Field looks and behaves identically whichever control drives it. For a form bound to react-hook-form with validation messages, use Form instead; Field is the lightweight, standalone layout.
Guidelines
Give every Field a label — that IS the control's name. The label is required and is associated by htmlFor, so you don't add a separate aria-label to the control. Add a description only when the effect isn't obvious from the label.
Use card for selectable options, plain for dense settings. A list of option cards (pick-one radios, feature toggles) reads well as card; a long settings screen is calmer as plain rows with a divider. The card's checked-edge highlight only makes sense when one option among several is chosen.
Match orientation to the control. Checkboxes and radios read naturally control-first (start); a switch usually sits at the end of a settings row (end), label on the left. Keep the whole group consistent.
One control per Field. Field clones a single child to wire it. For a group of radios, wrap each RadioGroupItem in its own Field inside one RadioGroup.
Best practices
- Pass exactly one control as children; let Field wire the label + description.
- Use
appearance="card"for pick-one option cards,orientation="end"for switch settings rows.
- Don't add an aria-label to the control — the Field's label already names it.
- Don't put multiple controls in one Field; wrap each in its own Field.
Content guidelines
- Label states what the setting is, in sentence case; the description is one short line, not a paragraph.
- Don't repeat the label in the description.
Troubleshooting
The control has no accessible name / axe flags it.
Cause. The Field's label is missing, or an aria-label on the control competes with it.
Fix. Always pass label; don't add aria-label to the control — Field names it via htmlFor.
The card doesn't highlight when selected.
Cause. The child control doesn't expose data-state=checked (or isn't checked).
Fix. Use a Switch / Checkbox / RadioGroupItem as the child — the card reads their data-state via :has().
Accessibility
Accessibility requirements
Pass exactly one control (Switch, Checkbox, or a RadioGroupItem) as children — Field clones it to inject the id + aria-describedby.
when children is not a single control element
- The Label's htmlFor points at the control's generated id, so the control's accessible name is the label text.
- A description is given an id and wired onto the control via aria-describedby (preserving any aria-describedby the control already had).
- Clicking the label toggles the control (native label association); the description is not part of the accessible name.