Click the row
Overview
The component lives in @flatpay-dk/ui and ships as two surfaces: <Switch> for the bare toggle (use it when a surrounding row already provides the label) and <SwitchField> for the canonical row from the Figma — label, optional description, toggle on the right, the entire row clickable.
Switch or checkbox
Reach for Switch when the change takes effect immediately — no Save button, no form submit. Reach for Checkbox when the value is part of a form that gets submitted later. The visual difference tells the user when the change actually happens.
Anatomy
A 64 × 32 px pill track holds a 24 × 24 px white knob with a 4 px inset. The knob translates 32 px between off and on. The whole thing renders inside a row that pairs it with a label and an optional description.
- Track. 64 × 32 px, fully rounded, 4 px inner padding. Off uses
bg-foreground/30; on flips tobg-foreground(charcoal). - Knob. 24 × 24 px white circle with a 1 px soft shadow. Translates 32 px on toggle, on a 150 ms ease-out-quart — the system entrance easing.
- Label. Inter Tight Medium 16 px primary. The field that names what the toggle controls.
- Description. Optional. Inter Tight Regular 14 px at
text-foreground/65. Use it to name the consequence in plain English — what changes when the user flips the toggle.
States
Off, on, hover (each side), focus, disabled-off, disabled-on. The colour ramp moves entirely inside the neutral scale — Switch never enters chroma, even when on.
Off · rest
Default unchecked.
On · rest
Default checked — track flips to charcoal.
Off · hover
Track lifts a step toward foreground.
On · hover
Track desaturates a step.
Focus
2 px blurple ring with a 2 px offset — drawn around the pill.
Disabled · off
Track drops to 15% foreground; click is refused.
Disabled · on
Track drops to 30% foreground; the locked-on case.
Switch vs. checkbox
Same job — pick one of two values — but different commitment. The shape signals when the change takes effect: a switch fires immediately, a checkbox waits for the form.
Switch
Takes effect immediately. Toggling Receipts to on starts printing receipts now — there’s no Save button.
Checkbox
Stages a value for a form. The change applies when the user submits — Save, Send, Continue.
In a settings list
The most common home for SwitchField — a vertical stack inside a card, separated by 1 px dividers between rows. Each row is its own tap target; clicking the description toggles the same as clicking the pill.
Behavior
- Toggle. Click anywhere on the row (label, description, or pill) to toggle. The change fires
onCheckedChange(next)with the next value. - Keyboard. Focus the row with Tab; press Space or Enter to toggle. The focus ring sits around the entire row in SwitchField, around the pill in bare Switch.
- Optimistic.The component is fully controlled. Update local state immediately on the change, then sync to the server in the background. If the network call fails, revert and surface the error — but don’t hold the toggle in transit.
Accessibility
- Role. Both surfaces render
role="switch"witharia-checked. Screen readers announce the toggle as a switch with an on/off state — the right pattern for instant settings. - Naming. SwitchField wires the visible label via
aria-labelledbyand the description viaaria-describedby. For bare Switch, passaria-labelwhen no text in the surrounding context names it. - Don’t double up.Don’t put a Switch inside a label that has its own click handler. The component is the click target; nesting it inside a clickable row creates ambiguity about what fires when.
Code
Two surfaces, both fully controlled. SwitchField is the canonical shape; Switch is the primitive when a row already provides labelling.
tsx
import { Switch, SwitchField } from "@flatpay-dk/ui";
// Canonical row — label + description + toggle
const [receipts, setReceipts] = useState(true);
<SwitchField
label="Receipts"
description="This printer will print out sales and refund receipts."
checked={receipts}
onCheckedChange={setReceipts}
/>
// Bare toggle — for table cells, list rows, anywhere a label is already provided
<div className="flex items-center justify-between">
<span>Auto-payouts</span>
<Switch
checked={auto}
onCheckedChange={setAuto}
aria-label="Auto-payouts"
/>
</div>
// Disabled — frozen state
<SwitchField
label="Demo mode"
description="Locked by your workspace admin."
checked={false}
disabled
/>Best practices
- Phrase the label as the thing being controlled, not the action. Receipts, not Print receipts. Auto-payouts, not Enable auto-payouts. The switch state already encodes the verb.
- Use the description for the consequence.Spell out what changes when the toggle flips — “This printer will print out sales and refund receipts.” Don’t repeat the label.
- Don’t use a switch inside a form that needs saving.If the user has to press Save for the change to apply, that’s a Checkbox. A switch promises immediacy; breaking that promise is worse than picking the wrong control.
- Avoid switches for destructive or irreversible actions.A switch is for low-stakes binaries. Anything that asks “are you sure?” needs a Button + a confirmation, not a one-click flip.
Props
<Switch>
| Prop | Type | Default | Description |
|---|---|---|---|
| checked* | boolean | — | Current state. The component is fully controlled. |
| onCheckedChange | (checked: boolean) => void | — | Fires with the next state when the user toggles via click or keyboard. |
| disabled | boolean | false | Refuses click and keyboard activation. |
| aria-label | string | — | Accessible name. Required when no surrounding text labels the switch. |
<SwitchField>
| Prop | Type | Default | Description |
|---|---|---|---|
| label* | ReactNode | — | Visible label. Inter Tight Medium 16 px. Phrase as the thing controlled, not the action. |
| description | ReactNode | — | Optional supporting line. Inter Tight Regular 14 px secondary. Spell out the consequence. |
| checked* | boolean | — | Current state. Fully controlled. |
| onCheckedChange | (checked: boolean) => void | — | Fires with the next state when the row is toggled. |
| togglePosition | "left" | "right" | "right" | Where the toggle pill sits relative to the text. Defaults to the Figma's right-side layout. |
| disabled | boolean | false | Refuses click and keyboard activation; drops the row to a quieter ramp. |