Files
ADS3-Design-System/plans/toggle-controls.md
Richie 07be9d7314 Add Input, Checkbox, Radio, and Switch form components with semantic token layer
Build four form primitives from Figma references with brand-aligned creative
decisions: restrained press states (scale-95 instead of highlight splashes),
clean iconless Switch, and consistent error states with inline warning icons.

Introduce form-control semantic tokens (--color-control-*) in tokens.css so
all form components share a single source for borders, checked states, focus
rings, labels, and errors. Retrofit Input to use these tokens instead of
direct palette references.

Update CLAUDE.md and ARCHITECTURE.md with token layer documentation, token
discipline rule (no palette references in components), and component tier
decision criteria.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-21 14:00:56 +10:00

2.9 KiB

Toggle Controls: Checkbox, Radio, Switch

Figma References

  • Checkbox: node-id=33-5043
  • Radio: node-id=33-5188
  • Switch: node-id=33-5337

What I'm Taking from Figma

  • Sizing: 20px checkbox/radio icon inside a clickable area, 16px body text labels
  • Colours: blue-01 (#002664) for checked state, grey-03 for unchecked border, grey-01 for labels
  • Layout: icon + label in a horizontal row with 8px gap
  • Disabled: 60% opacity

What I'm Changing (Per Your Brief)

Pressed/Active States (Checkbox & Radio)

The Figma designs use a large circular background highlight on press — feels heavy for a form control. Instead:

  • Hover: subtle border colour shift to blue-01 (unchecked) or slight darkening (checked)
  • Active/pressed: brief scale-down (scale-95) on the control — tactile without a big splashy highlight
  • Focus-visible: 2px blue-04 ring offset for keyboard nav (accessibility)

Switch

Figma has icons inside the thumb (check/X). Removing those per your request. My approach:

  • Track: 44px wide x 24px tall, rounded-full. Off = grey-03, On = blue-01
  • Thumb: 18px white circle, smooth slide transition (150ms)
  • Hover: track lightens/darkens slightly
  • Focus-visible: ring around the track
  • Clean, minimal look that matches the Input component's rounded-[4px] + blue-01 brand

Indeterminate (Checkbox Only)

Keeping the indeterminate state (dash icon) — useful for "select all" patterns. Same blue-01 fill as checked.

Props

Checkbox

interface CheckboxProps {
  label?: string
  description?: string       // secondary text below label
  checked?: boolean
  indeterminate?: boolean
  disabled?: boolean
  error?: string
  onChange?: (checked: boolean) => void
}

Radio

interface RadioProps {
  label?: string
  description?: string
  value: string
  disabled?: boolean
}

interface RadioGroupProps {
  label?: string              // group legend
  description?: string
  value?: string
  defaultValue?: string
  error?: string
  disabled?: boolean
  orientation?: 'vertical' | 'horizontal'
  onChange?: (value: string) => void
  children: React.ReactNode   // Radio items
}

Switch

interface SwitchProps {
  label?: string
  description?: string
  checked?: boolean
  disabled?: boolean
  onChange?: (checked: boolean) => void
}

Accessibility

  • Checkbox: hidden native <input type="checkbox"> with visual overlay, aria-checked, supports indeterminate
  • Radio: native <input type="radio"> within a <fieldset>/<legend> group, arrow key navigation
  • Switch: role="switch" with aria-checked, toggled via Space/Enter
  • All: focus-visible ring, disabled state, proper label association

Stories (per component)

  • Default, Checked, WithDescription, Disabled, DisabledChecked, WithError, AllStates
  • Radio adds: RadioGroup, Horizontal, WithError
  • Switch adds: Default, On, WithDescription, Disabled, AllStates