Switch

User Interface (UI)

How a switch looks, from its parts and sizes to its states and tokens.

Let's start with how a switch looks: its parts, its sizes, how it shows each of its states and the tokens it reads.

Anatomy

  1. Control — The <input> itself, which holds the state and takes focus. It lies over the track, invisible, and its label names it.
  2. Track — The bar the thumb moves along, an outline while the switch is off and filled while it's on.
  3. Thumb — The dot that sits at the start of the track while the switch is off, and at the end while it's on.
  4. Focus ring — An outline around the track, drawn outside it so nothing moves.

The root <span> around them carries the size and takes your classes. The label, and any hint or error message, sit outside it, in the page, and point to the control by its id (see Label every switch). The parts always render in the same order, which you can see in the DOM contract.

Sizes

The size prop scales the track and the thumb together, and md is the default. It takes the same five values as the Input and the Button, and every switch is shorter than the input of the same size, so it fits in a row of form controls of its size.

<label htmlFor="notifications">Notifications</label>
<Switch id="notifications" size="xs" defaultChecked />
{/* …sm, md, lg, xl */}
SizeTrackThumbHit area
xs28 × 16px10px28 × 24px
sm32 × 18px12px32 × 24px
md36 × 20px14px36 × 24px
lg44 × 24px18px44 × 24px
xl52 × 28px22px52 × 28px

Sizes are set in rem, and the table shows the values we recommend at the default text size of 16px. The control reaches past a small track to a hit area of at least 24 × 24px (WCAG 2.5.8), so even the smallest switch is easy to hit. On touch screens, the hit area grows to at least 44 × 44px, without the switch looking any bigger. Clicking the label flips the switch too, which makes for a much bigger target.

States

Each state has a look of its own, styled from the <input> that exposes it. That way, what people see and what assistive technologies report can never drift apart, and a switch can't look on without actually being on.

Off (try hovering, clicking or tabbing to it)
On
Disabled
Read-only (try flipping it)

Couldn't turn on price alerts. Try again.

Invalid
<label htmlFor="wifi">Wi-Fi</label>
<Switch id="wifi" />

<label htmlFor="bluetooth">Bluetooth</label>
<Switch id="bluetooth" defaultChecked />

<label htmlFor="sync">Sync</label>
<Switch id="sync" defaultChecked disabled />

<label htmlFor="sso">Single sign-on</label>
<Switch id="sso" defaultChecked readOnly />

<label htmlFor="alerts">Price alerts</label>
<Switch id="alerts" aria-invalid="true" aria-describedby="alerts-error" />
<p id="alerts-error">Couldn't turn on price alerts. Try again.</p>
StateSelectorRecommended look
Offinput:not(:checked)An outlined track, with the thumb at the start, both at 55% of the text colour
Oninput:checkedA track filled with --color--primary, with the thumb at the end, in --color--primary-contrast
Hover:hover, inside @media (hover: hover)While it's off, the track and the thumb at 80% of the text colour, up from 55%
Focusinput:focus-visibleA 2px outline, 2px from the track, in --color--focus
Disabledinput:disabled50% opacity and a not-allowed cursor
Read-onlyinput[aria-readonly="true"]A dashed border, in the text colour while it's on, unless it's invalid
Invalidinput[aria-invalid="true"]A 2px border in --color--error-text, which an off thumb takes too

Off and on differ by more than colour. The thumb moves from one end of the track to the other, and the track goes from an outline to a solid fill, so people who can't tell the colours apart can still see which way it's set (WCAG 1.4.1). The track and the thumb read each state from the <input> before them, with :checked, :disabled, aria-readonly and aria-invalid, so they never need a class or a state of their own.

None of the states change the switch's size or position. The thumb only moves inside the track, the invalid border's second pixel is drawn inside it, and the focus ring outside it. You'll find how each state behaves under Every state.

Tokens

The switch reads five theme tokens, which every component shares:

TokenUsed for
--color--neutral-textMixed into the track and the thumb while the switch is off
--color--primaryThe track while the switch is on
--color--primary-contrastThe thumb while the switch is on
--color--error-textThe border of an invalid switch
--color--focusThe focus ring, which is the same for every component

There's no color prop, so on is always the primary colour. While it's off, the track and the thumb are the text colour at 55%, like the Input's border, since they need 3:1 against the page, and 80% on hover. While it's on, the track needs 3:1 against the page, and the thumb 3:1 against the track.

With our reference tokens, from the Tailwind CSS palette, the switch that's off comes to 3.5:1 on white and 5.0:1 on zinc-900, the track that's on to 6.4:1 and 5.7:1, and the thumb on it to 6.4:1 on both. If you change the tokens, make sure to check the same pairs. You'll find the tokens themselves in the Button's tokens.css, which you can copy into your theme.

If you need to tune a switch, you can set its own variables, such as --switch--track--width, --switch--track--height or --switch--thumb--offset, from a class. The Switch's styles sit in the components cascade layer, so your class wins without any specificity tricks:

Settings.tsx
import styles from "./Settings.module.css";

export function Settings() {
  return (
    <>
      <label htmlFor="wifi">Wi-Fi</label>
      <Switch id="wifi" className={styles.long} />
    </>
  );
}
Settings.module.css
.long {
  --switch--track--width: 3rem;
}

UI Checklist

RuleLevelScopeRequirementBasisCheck
switch/contrastMustComponentThe track and the thumb have a contrast of 3:1 against what's next to them, on and off, and so does the focus ringWCAG 1.4.11 (AA)Contrast checker
switch/more-than-colourMustComponentOn and off differ by more than colour: the thumb moves to the other end, and the track fillsWCAG 1.4.1 (A)Review
switch/focus-visibleMustComponentFocus shows an outline at least 2px thick around the track, outside itWCAG 2.4.7 (AA), 2.4.13 (AAA)Keyboard
switch/target-sizeMustComponentThe hit area is at least 24 × 24px, and at least 44 × 44px on touch screensWCAG 2.5.8 (AA), 2.5.5 (AAA)Emulation
switch/resizableMustComponentThe track and the thumb are sized in rem, so they grow with the textOpen Components, beyond WCAG 1.4.4 (AA)200% zoom
switch/stable-sizeMustComponentNo state changes the switch's size or position, and the thumb only moves inside the trackOpen ComponentsVisual regression test
switch/state-selectorsShouldComponentStates are styled from the <input> that exposes them, with :checked, :disabled, aria-readonly and aria-invalidOpen ComponentsReview
switch/hover-capableShouldComponentHover styles only apply inside @media (hover: hover)Open ComponentsReview
switch/reduced-motionMustComponentThe thumb doesn't slide when prefers-reduced-motion: reduce is setWCAG 2.3.3 (AAA)Emulation
switch/forced-colorsMustComponentThe track, the thumb and the on, focus, disabled and read-only states stay visible in forced colours modeOpen ComponentsEmulation
switch/more-contrastShouldComponentThe track and the thumb are drawn at full strength when prefers-contrast: more is setOpen ComponentsEmulation
Copyright © 2026