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).
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.
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-pillA 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-pxframe,--silver-500off /--gold-500Catch-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-500at 22% via color-mix; frame goes--gold-500The 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-4disc,--silver-500off /--gold-500on, lifted by--shadow-mdNo 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-basewith--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-ringon :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-labeloraria-labelledby.roleandaria-checkedare set internally and can't be overridden. - ref: Ref<HTMLButtonElement>
- Forwarded to the underlying button.
Usage
import { Switch } from '@halo-compliance/ui'
<Switch
checked={enabled}
onCheckedChange={setEnabled}
aria-label="Enable notifications"
/>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.