Switch

User Experience (UX)

How a switch behaves for every person, input and setting, so it's accessible, predictable, clear in every state and adaptive.

Now let's look at how a switch behaves for every person, input and setting. A good switch is accessible, predictable, designed in every state and adaptive.

Accessible

Use the native element

A native <input type="checkbox"> gives you focus, Space to flip it, a label you can click, a state that screen readers announce and form submission, all for free. role="switch" keeps every one of them, and only changes what screen readers say, "on" or "off" rather than "checked" or "not checked", which is how the APG Switch pattern describes it. A <div> gives you none of them, so each one is one more thing to rebuild by hand.

{/* Do: you get focus, Space, a label you can click and a state screen readers announce, for free */}
<label htmlFor="wifi">Wi-Fi</label>
<input id="wifi" type="checkbox" role="switch" />

{/* Don't: there's no role, name, state or keyboard until you rebuild them */}
<div className="switch" onClick={toggle} />

The state is the checkbox's own checked, so there's no aria-checked to keep in sync, and a switch isn't a toggle button, so it doesn't take aria-pressed either. A switch is either on or off, with no mixed state, so indeterminate doesn't apply to it. You'll find why we built it on a checkbox rather than a <button role="switch"> in the FAQ.

Label every switch

The label is also the switch's accessible name. Screen readers read it along with the state, as in "Email me about replies, switch, off", voice control users say it out loud ("click Email me about replies"), and tests and agents use it to find the switch.

At most one email a day

<label htmlFor="replies">Email me about replies</label>
<p id="replies-hint">At most one email a day</p>
<Switch id="replies" aria-describedby="replies-hint" />
  • Every switch has a visible <label>, linked to it with for and the switch's id (WCAG 1.3.1, 3.3.2). Clicking the label flips the switch too, which makes for a much bigger target.
  • Write the label so that on means yes, like "Show previews" rather than "Hide previews", so people don't have to work out a double negative.
  • A hint, like what the setting changes, goes in the switch's description, which screen readers read after its name. You can do that with aria-describedby, pointing at the hint's id.
  • If you name a switch with aria-label or aria-labelledby, make sure the name includes the visible label, ideally at the start (WCAG 2.5.3).
  • The Switch doesn't make up IDs of its own (see Deterministic), so give it an id for its label to point at, and one to each hint and error message that points back at it.

The label names the setting, and stays the same whether the switch is on or off. The switch already says which one it is, so a label that changes, like "Notifications on" and "Notifications off", either repeats the state or, once someone has flipped it, leaves them wondering whether it says what the switch does or what it's done.

{/* Do: the label names the setting, and the switch says whether it's on */}
<label htmlFor="notifications">Notifications</label>
<Switch id="notifications" checked={on} onChange={(event) => setOn(event.target.checked)} />

{/* Don't: the label changes with the state, so it never names the setting */}
<label htmlFor="notifications">{on ? "Notifications on" : "Notifications off"}</label>
<Switch id="notifications" checked={on} onChange={(event) => setOn(event.target.checked)} />

Contrast and size

  • The track and the thumb need a contrast of at least 3:1 against what's next to them, in both states (WCAG 1.4.11). While it's off, that's the outline and the thumb against the page, and while it's on, the track against the page and the thumb against the track. The focus ring needs 3:1 as well.
  • Off and on differ by more than colour, in where the thumb sits and in whether the track is filled (WCAG 1.4.1).
  • The hit area is at least 24 × 24px (WCAG 2.5.8), and at least 44 × 44px on touch screens (WCAG 2.5.5). Clicking the label flips it too, so a row with a label and a switch is easy to hit anywhere.

Show focus

When a switch has focus, its track shows an outline in the same focus colour as every other component (WCAG 2.4.7). Like the Button's, it's at least 2px thick and drawn outside the track, and we recommend 2px for both. Browsers only match :focus-visible on a checkbox when it got focus from the keyboard, so the ring doesn't show after a click, while keyboard users always see where they are.

Predictable

Apply it right away

A switch changes a setting the moment people flip it, just like a light switch. There's no Save button to look for, and nothing lost if they leave the page. That's what tells a switch apart from a checkbox, which only takes effect when its form is sent, so if a setting waits for a Save button, use a Checkbox instead.

import { useState } from "react";

export function DarkMode() {
  const [on, setOn] = useState(false);
  const [status, setStatus] = useState("");
  const [error, setError] = useState("");

  async function change(value: boolean) {
    // The switch moves at once, and the status says when the change is saved.
    setOn(value);
    setError("");
    setStatus("Saving…");
    try {
      await api.save({ darkMode: value });
      setStatus(value ? "Dark mode is on" : "Dark mode is off");
    } catch {
      // Back to how it was, with a message that says what happened.
      setOn(!value);
      setStatus("");
      setError("Couldn't change dark mode. Try again.");
    }
  }

  return (
    <>
      <label htmlFor="dark-mode">Dark mode</label>
      <Switch
        id="dark-mode"
        checked={on}
        onChange={(event) => change(event.target.checked)}
        aria-invalid={error ? "true" : undefined}
        aria-describedby={error ? "dark-mode-error" : undefined}
      />
      {error && <p id="dark-mode-error">{error}</p>}
      <p role="status">{status}</p>
    </>
  );
}

api.save stands in for your own request, which saves the setting.

  • The switch moves as soon as it's flipped, while the change is saved, so people can see that it worked. Say when it's saved in a status message, which screen readers announce without moving focus (WCAG 4.1.3).
  • Keep the switch enabled while the change is saved. Disabling it would take its focus away, and keyboard and screen reader users would lose their place.
  • If the change fails, move the switch back, so it never shows a setting that wasn't saved, and say what happened next to it, as under Invalid.
  • Flipping a switch changes a setting, and nothing else. It doesn't move focus, open a page or submit a form (WCAG 3.2.2). If it does something people might not expect, like signing them out of their other devices, say so in its hint before they flip it.

Keyboard

KeySwitch
Tab, Shift+TabMoves focus to and from the switch
SpaceFlips it on or off

Space comes from the native checkbox, so it works before any script loads. The APG makes Enter optional for a switch, and a native checkbox doesn't flip with it, so we leave it as it is, which keeps every checkbox and switch on the page working the same way.

Every state

Disabled

A disabled switch can't be focused, flipped or submitted. disabled sets the native attribute, so the switch leaves the tab order, screen readers announce it as dimmed or unavailable when they read through the page, and its form leaves it out when it's sent.

We recommend disabling as little as you can, since a disabled switch can't explain why it's disabled, and keyboard users can't even reach it to find out. If people need to see a setting but not change it, make the switch read-only instead. If a setting only works when another one is on, like badges with notifications, say so in its hint. And never disable a switch while it has focus, like while its change is saved, since that takes its focus away.

Read-only

A read-only switch shows a setting that people can't change, like single sign-on that their organisation manages. It stays in the tab order, so people can reach it and hear its state, assistive technologies that support aria-readonly report it as read-only, and its form still sends it. We recommend a dashed border, which tells it apart from a switch you can flip. Off, it keeps its outline colour, and on, the dashes take the text colour, which stands out against the page around the fill, unless it's invalid, where the error colour wins.

HTML doesn't let a checkbox be read-only, since readonly doesn't apply to it, so the Switch does two things itself. It sets aria-readonly="true", which WAI-ARIA allows on a checkbox and so on a switch, and it cancels the click that would flip it. Space and a click on the label both click the <input>, so that one cancelled click covers the keyboard, the pointer and the label, and no change event fires.

Keep in mind that a read-only switch can't say why it's read-only, any more than a disabled one can, so say who can change the setting in its hint, linked with aria-describedby.

Invalid

A switch is invalid when its change didn't go through, like when saving it failed, or when the setting can't be turned on yet, like two-step verification without a phone number. The Switch shows the state, and the page decides when to show it, since only the page knows whether the change worked. You'll find one under States, and the code that sets it under Apply it right away.

  • Set aria-invalid="true" on the switch while its error message shows, and point aria-describedby at the message. Screen readers then announce the switch as invalid, and read the message after its name (WCAG 3.3.1).
  • Say what happened and what to do, as in "Couldn't turn on price alerts. Try again.", rather than "Error". A red border alone can't say any of that, and it means nothing to people who can't tell red apart (WCAG 1.4.1).
  • Clear the error as soon as people flip the switch again, since a new try makes the old message out of date.

Adaptive

Pointer, touch and pen

  • Hover styles only apply to pointers that can actually hover (@media (hover: hover)), otherwise a tap would leave the switch looking hovered.
  • The control lies over the track, and reaches past a small one to a hit area of at least 24 × 24px, and at least 44 × 44px on touch screens, without the switch looking any bigger.
  • We recommend leaving out dragging the thumb, which some switches copy from phones. A tap or a click flips it, which works with every pointer, while a drag asks for a movement that some people can't make (WCAG 2.5.7).

Motion

When someone prefers reduced motion (prefers-reduced-motion: reduce), the thumb jumps to the other end of the track rather than slides (WCAG 2.3.3). The slide is short, 150ms in ours, and it only plays when people flip the switch, but it's still motion, and the state is just as clear without it.

More contrast

When someone asks for more contrast (prefers-contrast: more), as with Increase contrast on macOS and iOS, the outline and the thumb of a switch that's off are drawn in the text colour at full strength, rather than at 55%. The switch that's on already passes 3:1, so it stays as it is.

Forced colours

Windows contrast themes replace every colour with the user's own, and drop backgrounds. That would leave the thumb and the filled track invisible, so the Switch keeps their colours with forced-color-adjust: none, and picks system colours for them: CanvasText for the outline and the thumb while it's off, Highlight for the track and HighlightText for the thumb while it's on, and GrayText while it's disabled. Focus is drawn with outline, in Highlight, an invalid switch keeps its 2px border, and a read-only switch keeps its dashes, in CanvasText while it's on.

Colour scheme

The tokens use light-dark(), which follows color-scheme, so the Switch adapts to your light and dark themes on its own. We've checked the contrast in both.

Text size and spacing

The track and the thumb are sized in rem, so they grow with the text at 200% text size (WCAG 1.4.4), and stay in proportion with their label. With wider line, letter and word spacing (WCAG 1.4.12), only the label grows, so let it wrap rather than giving it a fixed width.

Languages and direction

The thumb starts at the inline start of the track and moves to the inline end, set with logical properties, so in right-to-left languages it flips on its own, and on is on the left:

<div dir="rtl" lang="ar">
  <label htmlFor="wifi">واي فاي</label>
  <Switch id="wifi" defaultChecked />

  <label htmlFor="bluetooth">بلوتوث</label>
  <Switch id="bluetooth" />
</div>

Remember to translate labels, hints and error messages along with the rest of the page, and to mark any that are in another language with lang, so that screen readers pronounce them correctly (WCAG 3.1.2).

UX Checklist

RuleLevelScopeRequirementBasisCheck
switch/native-elementMustComponentIt's a native <input type="checkbox"> with role="switch", and never a <div>APG, Open Components, beyond WCAG 2.1.1, 4.1.2 (A)Unit test
switch/checked-stateMustComponentIts state is the checkbox's own checked, announced as on or off, with no pressed or mixed stateWAI-ARIA, APGUnit test
switch/visible-labelMustUsageEvery switch has a visible <label>, linked with forWCAG 1.3.1, 3.3.2, 4.1.2 (A)axe label
switch/label-in-nameMustUsageAn aria-label or aria-labelledby includes the visible label, ideally at the startWCAG 2.5.3 (A)axe label-content-name-mismatch
switch/stable-labelMustUsageThe label names the setting and stays the same whether the switch is on or offAPGReview
switch/describedShouldUsageHints, like what the setting changes, are linked with aria-describedbyWCAG 1.3.1 (A)Screen reader
switch/keyboardMustComponentTab reaches the switch, and Space and a click on its label flip itWCAG 2.1.1 (A), APGUnit test
switch/applies-immediatelyMustUsageFlipping it applies its setting right away, with no Save button, and a setting that waits for one is a checkbox insteadOpen ComponentsReview
switch/announce-savedShouldUsageWhile its change is saved, it keeps its new state and stays enabled, and a status message says when it's savedWCAG 4.1.3 (AA), Open ComponentsScreen reader
switch/no-context-changeMustUsageFlipping it never moves focus, opens a page or submits a form, unless its hint says so firstWCAG 3.2.2 (A)Review
switch/error-messageMustUsageAn invalid switch has an error message, in text and linked with aria-describedby, that says what happened and what to doWCAG 1.4.1, 3.3.1 (A)Screen reader
switch/invalid-stateMustBothA switch sets aria-invalid="true" while its error message shows, and only looks invalid when it doesWCAG 4.1.2 (A), Open ComponentsUnit test
switch/revert-on-failureShouldUsageWhen its change fails, it goes back to how it was, and its error message says soOpen ComponentsReview
switch/disabled-inertMustComponentA disabled switch can't be focused, flipped or submittedHTMLUnit test
switch/read-onlyMustComponentA read-only switch can be focused and is sent with its form, but doesn't flip, and sets aria-readonly="true"WAI-ARIAUnit test
switch/read-only-valuesShouldUsageA setting people need to see but can't change is read-only rather than disabled, with a hint that says who can change itOpen ComponentsReview
switch/keep-focusMustUsageA switch is never disabled while it has focus, like while its change is savedOpen ComponentsKeyboard
switch/mirrors-in-rtlShouldComponentThe thumb moves from the inline start to the inline end, so on is on the left in right-to-left layoutsOpen ComponentsReview

FAQ

Switch or something else?

A switch turns a setting on or off, right away. For anything else, another component will do a better job:

When people need to…Use
Turn a setting on or off, with an effect they see right awaySwitch
Pick options that are sent with a form, like agreeing to termsCheckbox
Turn an action on and off in a toolbar, like Bold or MuteToggle button
Pick one of a few settings, like Light, Dark or SystemRadio Group

The Checkbox takes the same size names and states as the Switch, and adds an indeterminate state, but its <label> wraps the box, where the Switch's is linked with for. The Radio Group is on our Roadmap, and it'll share the Switch's sizes, states and rules for labels.

Copyright © 2026