Components

Switch

The on/off toggle, official style: a flat pill track with a round knob. OFF keeps the track neutral with the knob parked left; ON floods the track solid gold — the same gold as the primary button — and the knob, now white, slides right. Quick, smooth, done. Built on a real <button role="switch">, so Space / Enter toggle it and assistive tech announces it as a switch with its on/off state.

Switch is the bare control. For a boolean form value — a labelled row with helper / error text and auto-linked ARIA — reach for SwitchField from the form layer instead of pairing a bare <Switch> with a loose <label> element.

States

Three states, each easy to tell apart. OFF is a flat neutral track with the knob parked left; ON fills the track solid gold and slides the knob right; disabled dims the whole control to half and locks the cursor. Focus brings the soft gold glow (shown statically below — tab to a live switch for the real thing). And the on/off call never rests on color alone: the knob's position carries it too, so it reads without color (ADR-0017).

state: off
Default. A flat, neutral track — no fill, no fuss — with the knob parked left.
state: on
The track fills solid gold and the knob turns white and slides right. One glance, no doubt.
state: disabled (off)
Dimmed to 50% with a not-allowed cursor; still reads off. Use for a setting that can't be turned on yet.
state: disabled (on)
Same dimming, but holds its on value — a locked-on setting the user can't turn off.
state: focus
A soft gold glow on :focus-visible. Shown statically here through the is-focused modifier; tab to any live switch for the real thing.

In a form

Inside a form, reach for SwitchField — the form-bound compound. It reads the field from TanStack Form context, binds checked / onCheckedChange to the boolean value, marks the field touched on blur, and wires the label / helper / error ARIA. Its control sits beside the label as a row — a toggle reads as a labelled switch, not a stacked field. The label is a real <label>, so clicking the text toggles the switch too.

A neutral hint sits here; an error would replace it.
Bound value: on

Materiality

Heads up: the token recipe below documents the proposed brand’s metal-on-glass build. The official switch keeps the same geometry — token-derived, border-box, knob travel computed from the interior — but swaps the materials: a flat, opaque track, a solid gold fill when on, a white knob, and a soft gold focus glow. No metal, no glass.

track
--space-8 × --space-5 (40 × 20px), pill-rounded via --radius-pill

A horizontal pill. Officially it's a flat, opaque surface — the metal-and-glass treatment named here belongs to the proposed system. Same shape, solid build.

frame + catch-light
--space-px frame, --silver-500 off / --gold-500

Catch-light and corner-of-brightness are proposed-system effects. The official track skips the metallic frame entirely: a hairline border on a flat fill, deepening to gold when on. Nothing glints; it doesn't need to.

on fill
--gold-500 at 22% via color-mix; frame goes --gold-500

The 22% wash is the proposed recipe. Officially, ON goes all in: the track fills solid gold — the same gold as the primary button — with a white knob on top. No tint, no translucency.

knob
--space-4 disc, --silver-500 off / --gold-500 on, lifted by --shadow-md

No metal puck here — the official knob is a simple solid disc, muted when off and white when on, riding a flat track. It still carries the state by position, so the toggle reads without color.

motion
Knob slides over --dur-base with --ease-mechanical — a settle, not a spring.

“A settle, not a spring” is proposed doctrine — the calibration metaphor lives there. The official slide is just quick and friendly: the knob glides over, the fill snaps in, and you're on. Reduced-motion stills it entirely (ADR-0012).

focus ring
--focus-ring on :focus-visible.

Keyboard focus gets the same soft gold glow the rest of the system uses; pointer clicks don't (focus-visible, not focus).

Props

checked: boolean
Controlled on/off state. Required — Switch has no uncontrolled mode; binary UI state should live somewhere nameable.
onCheckedChange: (checked: boolean) => void
Fired with the next state when the user toggles. Required. Update your state here.
disabled?: boolean
Dims the control to 50% and blocks the toggle.
aria-label, id, onBlur, name, …
Standard button attributes pass through. Standalone, a Switch needs an accessible name — pass aria-label or aria-labelledby. role and aria-checked are set internally and can't be overridden.
ref: Ref<HTMLButtonElement>
Forwarded to the underlying button.

Usage

Standalone — controlled, with an accessible nametsx
import { Switch } from '@halo-compliance/ui'

<Switch
  checked={enabled}
  onCheckedChange={setEnabled}
  aria-label="Enable notifications"
/>
Form-bound — SwitchField inside useHaloFormtsx
import { useHaloForm } from '@halo-compliance/ui'

const form = useHaloForm({
  defaultValues: { marketingEmails: false },
})

<form.AppField name="marketingEmails">
  {(field) => (
    <field.SwitchField
      label="Send me product updates"
      helperText="You can turn this off any time."
    />
  )}
</form.AppField>

When to use

  • A single setting that is on or off — a preference, a feature flag, a notification toggle. One thing, two states.
  • An instant-apply setting — the switch is the commit. No separate Save; flipping it does the thing.
  • A boolean form value that reads as enabled / disabled rather than a checkbox in a list of many.

Antipatterns

  • More than two options. A switch is strictly binary. For three-plus mutually exclusive choices, use Selector or a segmented control.
  • An action, not a state. Submit, delete, run — those are Buttons. A switch sets state; it doesn't perform a one-shot action.
  • Leaving it uncontrolled. There is no defaultChecked. Drive checked from state and update it in onCheckedChange.
  • A nameless standalone switch. Without a visible label, pass aria-label / aria-labelledby — or use SwitchField, which wires the label for you.
  • /button — actions vs. state; a switch sets, a button does.
  • /selector — when it is more than two options.
  • /input — the other everyday form control, and the TextField sibling of SwitchField.
  • /tokens--radius-pill, --focus-ring, and the duration / easing tokens behind the slide.