Components · Forms

Switch

A binary toggle for settings that take effect immediately. Use Switch when flipping the value is the action — turning a printer on, enabling tip prompts, switching demo mode.

Documentedby Derek Fidler

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 to bg-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.

Receipts on

Switch

Takes effect immediately. Toggling Receipts to on starts printing receipts now — there’s no Save button.

Send me a copy

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" with aria-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-labelledby and the description via aria-describedby. For bare Switch, pass aria-label when 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>

PropTypeDefaultDescription
checked*booleanCurrent state. The component is fully controlled.
onCheckedChange(checked: boolean) => voidFires with the next state when the user toggles via click or keyboard.
disabledbooleanfalseRefuses click and keyboard activation.
aria-labelstringAccessible name. Required when no surrounding text labels the switch.

<SwitchField>

PropTypeDefaultDescription
label*ReactNodeVisible label. Inter Tight Medium 16 px. Phrase as the thing controlled, not the action.
descriptionReactNodeOptional supporting line. Inter Tight Regular 14 px secondary. Spell out the consequence.
checked*booleanCurrent state. Fully controlled.
onCheckedChange(checked: boolean) => voidFires 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.
disabledbooleanfalseRefuses click and keyboard activation; drops the row to a quieter ramp.
On