Light

Radio Group

A set of checkable buttons—known as radio buttons—where no more than one of the buttons can be checked at a time.

Spec · from metadata

When to use

  • Selecting exactly one option from a small set (2-5 choices)
  • When all options should be visible at once
  • Form fields requiring explicit single selection

When not to use

  • Many options (>5) — use Select or Combobox instead
  • Multiple selections — use checkbox group instead
  • Binary toggle — use Switch instead
  • On/off state — use Toggle instead

Anti-patterns

Avoid<RadioGroup>
  <RadioGroupItem value="a" />
  <RadioGroupItem value="b" />
</RadioGroup>
Prefer<RadioGroup defaultValue="a">
  <Field orientation="horizontal">
    <RadioGroupItem value="a" id="opt-a" />
    <FieldLabel htmlFor="opt-a">Option A</FieldLabel>
  </Field>
  <Field orientation="horizontal">
    <RadioGroupItem value="b" id="opt-b" />
    <FieldLabel htmlFor="opt-b">Option B</FieldLabel>
  </Field>
</RadioGroup>

Every radio item needs a visible label. Use Field + FieldLabel for accessible pairing.

Accessibility

  • Required ARIAaria-label or aria-labelledby on RadioGroup
  • Min touch target16px
  • Screen readerBuilt on Radix RadioGroup which handles ARIA roles and keyboard navigation. Each item renders with role='radio'.
  • ContrastSelected indicator uses fill-primary for clear visual state.

Token bindings

TokenCategoryUsage
primarycolorSelected indicator fill
input-bgcolorRadio background
inputcolorRadio border
ringcolorFocus ring
destructivecolorError state
shadow-xsshadowRadio shadow

Import

import { Label } from "@timelycare/helix-ui"
import { RadioGroup, RadioGroupItem } from "@timelycare/helix-ui"

Props

interface RadioGroupProps {
  value?: string
  defaultValue?: string
  onValueChange?: (value: string) => void
  disabled?: boolean
  className?: string
}

interface RadioGroupItemProps {
  value: string
  id?: string
  disabled?: boolean
  className?: string
}

Structure

PartTailwind
RadioGroupflex flex-col gap-3
Item rowflex items-start gap-3
RadioGroupItemsize-4 rounded-full border border-border bg-input-bg shadow-xs
Selected indicatorsize-2 rounded-full bg-primary (centered)
Labeltext-sm font-medium leading-none text-foreground
Descriptiontext-sm text-muted-foreground

Types

TypeUse ForImplementation
DefaultStandard selection listsRadio + label inline
BoxProminent choices, cardsWrap in bordered container with border rounded-lg p-4 bg-input-bg

States

StateImplementation
Defaultborder-border bg-input-bg
SelectedInner circle bg-primary visible
Focusfocus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2
Disableddisabled:opacity-50 disabled:cursor-not-allowed

Common Patterns

Basic Radio Group

<RadioGroup defaultValue="option-1">
  <div className="flex items-start gap-3">
    <RadioGroupItem value="option-1" id="option-1" />
    <Label htmlFor="option-1">Option One</Label>
  </div>
  <div className="flex items-start gap-3">
    <RadioGroupItem value="option-2" id="option-2" />
    <Label htmlFor="option-2">Option Two</Label>
  </div>
</RadioGroup>

With Descriptions

<RadioGroup defaultValue="comfortable" className="flex flex-col gap-3">
  <div className="flex items-start gap-3">
    <RadioGroupItem value="default" id="r1" className="mt-0.5" />
    <div className="flex flex-col gap-1.5">
      <Label htmlFor="r1" className="font-medium leading-none">Default</Label>
      <p className="text-sm text-muted-foreground">
        This is a radio description.
      </p>
    </div>
  </div>
  <div className="flex items-start gap-3">
    <RadioGroupItem value="comfortable" id="r2" className="mt-0.5" />
    <div className="flex flex-col gap-1.5">
      <Label htmlFor="r2" className="font-medium leading-none">Comfortable</Label>
      <p className="text-sm text-muted-foreground">
        This is a radio description.
      </p>
    </div>
  </div>
</RadioGroup>

Box Style

<RadioGroup defaultValue="card" className="flex flex-col gap-3">
  <Label
    htmlFor="card"
    className="flex items-start gap-3 border rounded-lg p-4 bg-input-bg cursor-pointer has-[[data-state=checked]]:border-primary"
  >
    <RadioGroupItem value="card" id="card" className="mt-0.5" />
    <div className="flex flex-col gap-1.5">
      <span className="font-medium text-sm leading-none">Card Option</span>
      <span className="text-sm text-muted-foreground">This is a radio description.</span>
    </div>
  </Label>
</RadioGroup>

Accessibility

  • Built on Radix RadioGroup — handles keyboard and focus automatically
  • Keyboard: Arrow keys to navigate options, Space to select
  • Screen reader: announces group label and selected option
  • Requires label association via Label component
  • Uses roving tabindex for focus management

Gotchas

ProblemSolution
Label not clickableWrap with <Label htmlFor> matching radio id
Radio misaligned with multilineAdd mt-0.5 to RadioGroupItem
Box not highlighting on selectUse has-[[data-state=checked]]:border-primary
Horizontal layout neededChange RadioGroup to flex-row

See Also


Last updated: February 9, 2026