Skip to content

Switch

An instant on/off toggle for a single setting.

Stablev0.4.2added in v0.1.0@garn/ui/switch
On this page

Default

Default switch.

States

Interactive states, toggled live.

Labeled

Switch with a setting label.

Sizes

sm · md · lg.

Tones

The tone axis: neutral · brand · success · warning · danger.

Content

In-track glyphs via checkedContent / uncheckedContent.

Controlled

Controlled `checked` + `onCheckedChange`, driving `loading` while an async toggle resolves.

Field

Switch in a Field — settings row + option card.

Installation

garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.

terminal
npx garn-ui add switch
First time? Set up garn in your project
Registry
switch
Deps
@radix-ui/react-switchclass-variance-authoritylucide-react
Registry deps
utils

Import

import { Switch } from "@garn/ui/switch";

Props

The component's public props and their types.

size
Description
No description.
Type
"lg" | "md" | "sm"
Default
md
tone
Description
No description.
Type
"brand" | "danger" | "neutral" | "success" | "warning"
Default
brand
checkedContent
Description
Icon or 1–2 chars shown in the track well while ON (the vacated inline-start side).
Type
React.ReactNode
loading
Description
Shows a spinner in the thumb and blocks interaction (sets `disabled` + `aria-busy`) — for a toggle whose effect resolves asynchronously.
Type
boolean
Default
false
uncheckedContent
Description
Icon or 1–2 chars shown in the track well while OFF (the vacated inline-end side).
Type
React.ReactNode

Plus 293 inherited native <button> attributes.

Styling

Target these data-slots and states, and remap these tokens, to restyle without forking the component.

[data-slot="switch"]

States

SelectorState
[data-state=checked]checked
[data-state=unchecked]unchecked
[data-loading]loading
:focus-visiblefocus-visible
[disabled]disabled
PropertyTokenTier
trackHeight--garn-box-mdsemantic · identity
fill--garn-brand-solidbrandablesemantic
fillEdge--garn-brand-strongbrandablesemantic
fillOff--garn-mutedsemantic
ring--garn-ringbrandablesemantic

Private instance vars (never externalize): --sh

When to use

A setting that applies immediately — no Save step (notifications on/off, dark mode).

Reach for something else when

  • A choice submitted with a form on Save (use a checkbox)
  • mutually exclusive options (use radio-group).

Overview

Switch flips a setting that takes effect the instant it changes — notifications on/off, dark mode, an enabled feature — with no Save step. The filled track and travelled thumb make the on/off state legible at a glance. If the choice is only confirmed later on submit (a form value), use a Checkbox; if there are more than two states, use a RadioGroup or Select.

Guidelines

Use a switch only when the change is immediate. The mental model is a physical toggle: flipping it does the thing now. If you find yourself wanting a Save button for a screen of switches, those should probably be checkboxes instead.

Label the setting, not the state. The Label says what the switch controls ("Email notifications") — the on/off position already communicates the state, so don't append "on/off" to the text. Reserve switches for clearly binary settings.

Reach for tone only when the state carries a status. The default brand fill is right for almost every toggle. Use success/warning/danger when the ON position itself signals something (a risky feature flag, a destructive automation) — not for decoration.

Use loading for async toggles. When flipping runs a request, set loading to show the in-thumb spinner and block interaction (it sets aria-busy + disabled) until the real result lands — then reflect it, reverting if it failed. Don't leave the switch looking done while the effect is still in flight.

Pair it with a Field for anything more than a bare toggle. A description, a card surface, or a settings row is the Field's job — drop the Switch in as the Field's control so the label, htmlFor, and aria-describedby are wired for you.

Best practices

Do
  • Use for instant-effect settings; reflect the new state without a confirm step.
  • Pair with a label that states what the ON position means.
Don't
  • Don't use a switch inside a form that the user must submit — that's checkbox territory.
  • Don't pair a switch with a separate Save button for the same setting.

Content guidelines

  • Label the setting by what ON does, as a short statement (“Email notifications”), not “On/Off”.
  • Use sentence case and a few words; add a one-line description only when the effect isn't obvious.
  • Don't restate the state in the label — the switch position already shows on or off.

Troubleshooting

The switch flips back instantly / won't move.

Cause. Passing checked without onCheckedChange makes it controlled and frozen.

Fix. Use defaultChecked for uncontrolled, or pass checked + onCheckedChange.

No glyph shows inside the track.

Cause. In-track content is opt-in — there is no default glyph.

Fix. Pass checkedContent (and/or uncheckedContent) with an icon or 1–2 characters.

A tone other than brand doesn't change the OFF track.

Cause. tone only colours the ON (checked) track; the OFF track is a neutral surface for every tone.

Fix. That's intended — the tone reads as the on-state colour intent, not an always-on tint.

Thumb travel is wrong in RTL.

Cause. Built-in rtl: compiles to zero-specificity :where() and can lose to the base translate.

Fix. The component uses explicit [[dir=rtl]_&]: variants — don't replace them with plain rtl: utilities.

Accessibility

Role
switch
ARIA APG
switch
Focus
outline-hidden + 2px ring with a 2px offset.

Accessibility requirements

warn

Give every switch an accessible label (a <label htmlFor>, or aria-label).

when no associated <label> or aria-label/aria-labelledby

Keyboard

SpaceEnterToggle on / off.
A1.3.1Info and RelationshipsA2.1.1KeyboardA4.1.2Name, Role, Value
  • role=switch with aria-checked; data-state reflects checked / unchecked.
  • `loading` sets aria-busy and disables the control while the async effect resolves.
  • checkedContent / uncheckedContent are decorative (aria-hidden) — the state is conveyed by aria-checked, not the glyph.