# Toggle switches that don't say whether they are on

Source: https://easeweb.dev/learn/toggle-switches
Topics: Custom inputs > Toggle switches announce whether they are on or off (WCAG 4.1.2)
The fix: Either style a native checkbox to look like a switch, or give the custom control role="switch" with aria-checked, or make it a toggle button with aria-pressed. Whichever you choose, keep the exposed state in step with what is drawn and keep the name fixed.
Test with: keyboard, screen reader, forced colors, mouse
References: https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html https://www.w3.org/WAI/ARIA/apg/patterns/switch/ https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/switch_role

This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.

## What happens

An account settings page has a row of switches: email updates, text reminders, a newsletter. Each one is a button with a pill-shaped track and a round knob. Clicking it slides the knob and turns the track from grey to green. A screen reader user tabs to the first switch and hears "Email updates, button". They press Space, the knob moves, and they hear nothing new. They have no way to tell whether email updates are now on or off.

## Why it fails

Success criterion 4.1.2 requires that the state of a user interface component can be determined by software, and that assistive technology is told when it changes. Here the state lives only in a CSS class that draws the knob on one side or the other. The button has a name and a role, but nothing in the accessibility tree says on or off, so the change is drawn but never announced.

The same failure shows up as a `div` with `role="switch"` and no `aria-checked`, which axe reports as a missing required attribute. It also shows up as a switch whose `aria-checked` is set once on page load and never updated.

## Who is affected

Settings pages often stack several switches in a list, and people read them in order to check what is set. When the state isn't exposed, the whole list becomes a guess.

## What automation and AI miss

A scanner flags `role="switch"` without `aria-checked`, but a plain `button` with a CSS class passes every automated check: it has a role and a name, and nothing marks it as a switch. No scanner can tell that the class means on, or that the attribute still matches after a toggle. Code assistants often fix the label and leave the state alone, or swap the label between "On" and "Off", which changes the name instead of reporting a state. Toggle the switch and listen.

## Before: a button that draws its state

```html
<button type="button" class="switch" onclick="this.classList.toggle('is-on')">
  Email updates
</button>
```

The button can be reached with Tab and pressed with Enter and Space, but its state is only the `is-on` class. A screen reader announces "Email updates, button" whether it is on or off, and says nothing when it changes.

## After: a native checkbox styled as a switch

```html
<label class="switch">
  <input type="checkbox" name="email-updates" checked>
  Email updates
</label>
```

```css
.switch input {
  appearance: none;
  /* the track */
  inline-size: 2.75rem;
  block-size: 1.5rem;
  border: 2px solid currentColor;
  border-radius: 999px;
}
.switch input::before {
  /* the knob, moved by :checked */
  content: "";
  display: block;
  inline-size: 1rem;
  block-size: 1rem;
  margin: 0.125rem;
  border-radius: 50%;
  background: currentColor;
}
.switch input:checked::before {
  translate: 1.25rem 0;
}
```

The browser holds the state, exposes it as checked or not checked, and toggles it with Space and a click on the label. The CSS draws the switch from `:checked`, so the picture can't drift from what is announced. It also submits with the form, without script.

## After: a custom switch with aria-checked

```html
<button type="button" role="switch" aria-checked="true" class="switch">
  Email updates
</button>
```

```js
for (const toggle of document.querySelectorAll('[role="switch"]')) {
  toggle.addEventListener('click', () => {
    const on = toggle.getAttribute('aria-checked') === 'true';
    toggle.setAttribute('aria-checked', String(!on));
  });
}
```

```css
.switch[aria-checked="true"] { /* the knob on the right */ }
```

The switch role is announced as a switch, and `aria-checked` gives its state as on or off. Draw the knob from the attribute, not from a separate class, so one value drives both the picture and the announcement.

## After: a toggle button with aria-pressed

```html
<button type="button" aria-pressed="true" class="switch">
  Email updates
</button>
```

```js
for (const toggle of document.querySelectorAll('button[aria-pressed]')) {
  toggle.addEventListener('click', () => {
    const on = toggle.getAttribute('aria-pressed') === 'true';
    toggle.setAttribute('aria-pressed', String(!on));
  });
}
```

`aria-pressed` turns the button into a toggle button, announced as pressed or not pressed. It suits a control that looks and behaves like a button, such as a toolbar's Bold, more than a settings row, but it passes the criterion either way.

## Which option to use

All three pass. Use the native checkbox unless you have a reason not to: the browser keeps the state for you, so no script can leave it out of step, and it works inside a form with no extra code. That is the reason to recommend it for nearly every settings switch. Choose `role="switch"` when the component is already a custom button, or when you want it announced as a switch rather than a checkbox, and accept that the script must update `aria-checked` on every change. Choose `aria-pressed` when the control is really a toggle button in a toolbar, not a setting with an on and off position.

## Implementation decisions

A checkbox is announced as "checked" or "not checked", not "on" or "off". Most people understand that, but if you want switch wording you can add `role="switch"` to the checkbox itself: browsers then announce on and off and keep the native keyboard and form behaviour. Safari's `switch` attribute on a checkbox does the same, but only in Safari, so don't rely on it alone.

Whichever markup you use, keep these points in mind:

- Keep the name fixed. "Email updates" with a state of on is clear; a label that changes from "Turn on email updates" to "Turn off email updates" while also reporting a state contradicts itself.
- Show the state without colour alone. A knob that moves position, or an "On" and "Off" word drawn beside the track with `aria-hidden`, still reads in forced colors and for people who can't tell green from grey.
- Decide when the change takes effect. A switch usually applies at once; if it saves to a server, announce failures in a status region and set the state back, rather than leaving the switch showing a value that wasn't saved.

Component libraries often ship a switch with the state on a wrapper `div`. Check the element that takes focus, because that is the one whose state is read.

## Verify the fix

1. With a screen reader, Tab to each switch and confirm it announces the name, the role (checkbox, switch or toggle button) and the current state.
2. Toggle it and confirm the new state is announced, then reload the page and confirm the announced state matches what is drawn.
3. Toggle it by clicking, with Space, and, for a button, with Enter; confirm each changes the state exactly once.
4. Confirm the accessible name is the same in both states, and the state is not repeated in the name.
5. In forced colors, confirm on and off can still be told apart; with voice control, toggle the switch by saying its visible label.

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the shared switch component changes.

## Limits

This is a starting pattern, not a guarantee for every application. Screen readers word the switch role differently, and some older ones announce it as a checkbox or toggle button. The CSS is a sketch; check the focus indicator and contrast of the track against your own palette. Test the complete settings journey with your target assistive technology.
