Accessible Styled Form Controls

Switch Component: Checkbox

Published:

Last updated: May 11, 2021

Pattern to modify a checkbox into a switch component.

Pattern Demo

Version: [type="checkbox"] styled to look like a switch

Version: [type="checkbox"][role="switch"]

Version: [type="checkbox"][role="switch"] - Alternate Markup/Style

Visual design notes

These notes were last updated in August 2020.

-moz-appearance: none; did not completely remove the styling in earlier versions of Firefox. But it should work as expected in Firefox 60+.

The box-shadow to provide an outline of the control does not render as expected in IE11, did work as expected in Edge (legacy).

IE and Edge (legacy) don't respect the box-shadow in High Contrast mode. Previously, the ::-ms-check was re-enabled so that people will at least see a checkmark when the switch is on, and an empty oval when off. Chromium Edge does respect the box-shadow fill, so this has been updated. I no longer have Legacy Edge to verify if this introduces an issue there or not.

Pattern Details

Pattern Markup
<!-- Version: [type="checkbox"] styled to look like a switch -->
<label class="check-switch" for="switch_1">
  Example Text 1:
  <input id="switch_1" type="checkbox">
  <span aria-hidden="true"></span>
</label>

<!-- Version: [type="checkbox"][role="switch"] -->
<label class="check-switch" for="switch_2">
  Example Text 2:
  <input id="switch_2" data-check-switch="" role="switch" type="checkbox">
  <span aria-hidden="true"></span>
</label>

<!-- Version: [type="checkbox"][role="switch"] - Alternate Markup/Style -->
<div class="switchbox">
  <label for="switchbox">
    Example Text 3:
  </label>
  <input id="switchbox" data-check-switch="" role="switch" type="checkbox">
</div>

Similar to the styled checkbox pattern, an input type=checkbox can be used to create a custom switch control. A switch and checkbox behave similarly.

As the ARIA specification notes:

A switch provides approximately the same functionality as a checkbox and toggle button, but makes it possible for assistive technologies to present the widget in a fashion consistent with its on-screen appearance.

That said, a switch can often be found in "settings" UI, where toggling the switch's state can immediately cause a function or state change to occur. A checkboxes are commonly used (have a very long history) within forms – where toggling its state is not expected to result in an immediate function until the form itself is submitted. That's not to say that toggling checkboxes cannot also be used to cause an immediate action to occur. For instance, using checkboxes as controls to filter search results on an e-commerce site. But, this is not an "official" difference between the controls – more just me trying to point to common use to indicate a reason as to why the heck we'd need two toggle controls aside from aesthetic reasons...

That all said, if implementing visually styled switches within your project, their implementation should be consistent across instances. Meaning that if a checkbox is visually designed to look like a switch (but continued to be announced as a checkbox), then there should be no instances where the same design is used for a role="switch". This would create confusion in why two similar looking controls are announced differently (for sighted users who also may use screen readers), or perform actions immediately or delayed until form submission.

The JavaScript

There is little JavaScript that is needed for this component, but what is there is largely for progressive enhancement purposes.

Using the data-check-switch attribute as a hook, an input with this attribute will receive a role="switch" when the script runs. Without JavaScript, it will announce itself as a checkbox.

The script also allows for the Enter key to be used to toggle the state of the switch, as checkboxes would otherwise only allow for the Space key to toggle their state.

Affects on Screen Reader Announcements?

Giving the input type="checkbox" a role="switch" announces the form control as a "switch", "toggle button" or continues to announce the control as a "checkbox" depending on the screen reader and platform in question.

The following is a break down of how this is handled by modern browsers and screen reader pairings.

Firefox 88.0.1
With VoiceOver on macOS 11.2.3: announces "switch" role and initial state. Activating the switch will produce a "clicking" noise, but no "on" or "off" announcement is explicitly made.
With NVDA 2020.4: announces "checkbox" role and initial state. Activating the 'switch' will announce the new state ("checked" or "not checked"). - Note: NVDA
With JAWS 2020: announces "switch" role and initial state. Activating the switch will announce the new state ("on" or "off").
With TalkBack Android 11: announces "switch" role and initial state. Activating the switch will produce a sound effect, and the new state ("checked", "not checked") will be announced, oddly followed by a "loading" announcement.
Chrome / Edge 90
With VoiceOver on macOS 11.2.3: announces "switch" role and initial state. Activating the switch will produce a "clicking" noise and the new state ("on" or "off") will be announced.
With NVDA 2020.4: announces "checkbox" role and initial state. Activating the 'switch' will announce the new state ("checked" or "not checked").
With JAWS 2020: announces "switch" role and initial state. Activating the switch will announce the new state ("on" or "off"). When switching to the "on" state, JAWS will announce "pressed on". JAWS does not announce "pressed" when changing to the "off" state.
With TalkBack Android 11: announces "switch" role and initial state. Activating the switch will produce a sound effect, and the new state ("checked", "not checked") will be announced.
Edge 90
With Narrator: announces "switch" role and initial state. Activating the switch will announce the new state ("on" or "off"), along with a repeat of the switch's accessible name.
Safari
With VoiceOver on macOS 11.2.3 (Safari 14.0.3): announces "switch" role and initial state. Activating the switch will produce a "clicking" noise and the new state ("on" or "off") will be announced, along with a repeat of the switch's accessible name.
With VoiceOver on iOS 14.4.2: announces as "checkbox" role and initial state. Activating will produce a "clicking" noise and the new state ("checked" or "unchecked") will be announced.

Usage note:

Things have improved with switches since my initial testing of this role back in 2018, where state announcements were quirky if not missing all together (archived testing results below). NVDA and iOS Safari/VoiceOver are the hold outs on respecting the switch role, but at least these announce the switch as a checkbox with proper states.

Archived Screen Reader Breakdowns
JAWS 2020 + Firefox 77 & Chrome and Edge 83

Announces as a "switch". Each announce "on" and "off" when toggling state. Chrome alone will announce "pressed on" for the on state.

Narrator & Edge (legacy)

Announces as a "toggle switch". Announces "on" and "off" when toggling state.

VoiceOver + Safari on iOS 12.2

iOS VoiceOver continues to announce role="switch" as a "checkbox". However, with the release of iOS 12.2 (Mach 2019) the announcement of state has been fixed and "switches" will announce a "checked" or "unchecked" state.

Until iOS 12.2+ has wide adoption, one should still be wary of using role="switch".

VoiceOver + Safari on iOS 11.3 (at least) through 12.1.4

iOS VoiceOver doesn't understand role="switch" and will fallback to announcing the component as a "checkbox". However, this fallback is bugged, as the state of the checkbox will not be announced to users on toggle of the component.

Narrator & NVDA 2018.4.1 + IE11

This pairing will ignore the role="switch", and continue to announce the component as a checkbox. Checkbox state will be accurately announced on toggle.

JAWS 2018, 2019, 2020 + IE11

This pairing will announce the form control as a role="switch", but is inconsistent with announcing the state of the component. Largely only announcing state on a user first focusing the form control with by the virtual cursor navigation or tab key. If activating the form control repeatedly, focus will be lost and return to an element higher up in the DOM.

NVDA 2020.1 + Firefox 77, IE11, Chrome 82 and Edge 83

role="switch" is ignored and the control is announced as a checkbox. State is accurately announced in regards to being announced as a checkbox.

NVDA 2018.4.1 + Chrome 72

role="switch" is announced as a "toggle button." Toggling announces as "pressed checked" and "not pressed" for their respective states.

NVDA 2018.4.1 & 2019.1beta + Firefox 65.0.1

role="switch" is announced as a "toggle button." Toggling announces as "checked" and "not checked" for their respective states.

If "pressed" NVDA will announce initial state as "toggle button not pressed, checked". Toggling state will only announce "not checked" and "checked". (Closed bug where apparently NVDA decided to forgo announcing role=switch and expose it as a checkbox.)

TalkBack 7.2 (on Android 8.1) + Firefox 64.0.2 & Chrome 71

Announces as a "switch." Toggling announces as "checked" and "not checked" for their respective states.

NVDA 2018.1 and 2018.2.1 notes

These versions of NVDA paired with FireFox and Chrome will announce elements with role="switch" as "not pressed" and then announce whether they are checked or not.

It doesn't matter the current state of whether the switch is checked or not, with this version of NVDA the switch would always announce "not pressed."

This issue appears to have been fixed after testing with NVDA 2018.4.1.

Continue Reading

For an alternate minimal pattern to create switch checkboxes, and support for intermediate state, check out Adrian Roselli's Togglish Checkboxen CodePen, and his much longer write-up Under Engineered toggles.