Switching It Up With HTML’s Latest Control
switch
attribute to checkbox inputs. Development has been years in the making, but Safari recently pushed the matter by releasing support for the attribute — as well as new pseudo-elements for styling hooks — in Safari Technology Preview 185 and Safari 17.4. Daniel Yuschick walks us through a first impression of switch controls and discusses current and ongoing considerations that need to be explored further before it is ready for prime time.The web is no stranger to taking HTML elements and transforming them to look, act, and feel like something completely different. A common example of this is the switch, or toggle, component. We would hide a checkbox beneath several layers of styles, define the ARIA role as “switch,” and then ship. However, this approach posed certain usability issues around indeterminate states and always felt rather icky. After all, as the saying goes, the best ARIA is no ARIA.
Well, there is new hope for a native HTML switch to catch on.
Safari Technology Preview (TP) 185 and Safari 17.4 released with an under-the-radar feature, a native HTML switch control. It evolves from the hidden-checkbox approach and aims to make the accessibility and usability of the control more consistent.
<!-- This will render a native checkbox --//>
<input type="checkbox" />
<!-- Add the `switch` attribute to render a switch element --//>
<input type="checkbox" switch />
<input type="checkbox" checked switch />
Note: According to the WebKit blog, we can use @supports(::thumb)
to check for browser support. That said, ::thumb
is an experimental feature that requires the “::thumb
and ::track
pseudo-elements” features flag enabled in Safari. Otherwise, you may see a false positive where the support notice is displayed in the demo.
The default styles in Safari, unsurprisingly, feel like an iOS switch. Luckily, in addition to the new switch
attribute, we are also treated to new pseudo-elements for styling our switch to be the prettiest switch on the web.
Styling It Up With Pseudo Elements
Along with the rollout of the switch
attribute comes support for the new ::thumb
and ::track
pseudo-elements. We can use these pseudo-elements not only to style our but also to conditionally check support in CSS. But let’s not get ahead of ourselves, one thing at a time.
Styling.
The switch control by default supports the accent-color
and color-scheme
properties. This already gives us good theming support that is sort of out of the box. But we can gain full control over the switch by styling its pseudo-elements.
Base Input Styles
You must add appearance: none
to the switch before styling.
input[type="checkbox"][switch] {
appearance: none;
}
With the appearance removed, we can now style out control as far as our imagination takes us. Of course, with great power comes great responsibility. By removing the appearance, we are also responsible for positioning the parts for the on and off states.
The styles we apply to the base input can be treated as the container styles for the pseudo-elements. But be cautious, as styles applied here will likely become the fallback styles in unsupported environments.
input[type="checkbox"][switch] {
/* Required to override and style the switch */
appearance: none;
display: inline-grid;
position: relative;
background-color: var(--color-aux-400);
block-size: 2rem;
inline-size: 4rem;
border-radius: var(--border-radius-pill);
transition: background 0.25s ease;
padding-inline: var(--spacing-1x);
}
Adding more flexibility to the mix, styling the control itself allows us access to its own ::after
and ::before
pseudo-elements as well.
The ::thumb
Pseudo Element
The new ::thumb
pseudo-element allows us to style the handle of the switch. This is generally the element a person interacts with when toggling the control. A circular thumb may be the most common, but our styles are not limited there. But again, let’s focus on one thing at a time.
input[type="checkbox"][switch] {
appearance: none;
display: inline-grid;
position: relative;
background-color: var(--demo-color-aux-400);
block-size: 2rem;
inline-size: 4rem;
border-radius: var(--demo-border-radius-pill);
transition: background 0.25s ease;
padding-inline: 0.25rem;
/* Style the thumb */
&::thumb {
block-size: 1.5rem;
inline-size: 1.5rem;
border-radius: var(--border-radius-circle);
background: var(--color-aux-600);
transition: translate 0.25s ease;
}
/* Define styles for the 'on' state of the switch */
&:checked {
background: var(--color-blue-400);
}
&:checked::thumb {
/* We are responsible for moving the thumb between states */
translate: 2rem 0;
}
}
This demo experiments with applying transitions and animations to the ::thumb
element. Because applying appearance: none
makes us responsible for positioning the thumb for its different states, we also have the freedom to transition between these states however we’d like.
The ::track
Pseudo Element
While similar to styling the control itself, the ::track
pseudo-element can serve as the background track along which the thumb moves. Of course, in classic web fashion, this track doesn’t have to be styled as such, and this extra element can be creatively used in other ways.
Granted, these examples are rather basic. But the hope is that the potential is visible in the immense styling control we now have for this control. Of course, all this styling is wonderful, but first, we should check for support before putting all of our eggs into one proverbial basket.
Detecting It Up For Switch Support
As with all cool new things, there come less-than-cool considerations. In this case, the switch
attribute is supported only by Safari. If it catches on, we will see this element make its way into other major browsers, but until then, we need to detect if we can render the switch before committing to it.
Detecting With CSS
We can use CSS to detect support for the switch
attribute, but not as directly as I’d prefer. Because the switch element comes along with the ::thumb
and ::track
pseudo-elements, we can check for their support to then infer whether the switch itself is supported.
@supports selector(::thumb) {
/* The switch is supported. Style away! */
}
Or, if you want to default to switch-supported styles, you can conditionally check for the inverse by using the not
keyword.
@supports not selector(::thumb) {
/* The switch is _not_ supported. Fallback styles go here. */
}
Detecting With JavaScript
There may be a need to detect the use of the switch
attribute outside of CSS. For this, we can create an input element and check for the switch
attribute pin JavaScript. If the attribute exists, we will add a class to the root element of the document for CSS styling later.
function checkForSwitchSupport() {
const input = document.createElement('input');
input.type = 'checkbox'
if ('switch' in input) {
return true;
}
return false
}
if (checkForSwitchSupport()) {
document.documentElement.classList.add('has-switch');
}
.has-switch input\[type="checkbox"\][switch] {
accent-color: var(--brand-color-accent-primary);
}
Switch Considerations
With any new feature, there is a period where we have to learn how it works and how to use it. When that feature is particularly experimental and only supported by one of the major browsers, there is a bit more nuance to uncover and understand.
Checkboxes: The Same But Different
Adding a switch
attribute to a checkbox input is nice and convenient, but it comes with some assumptions. At the core, I believe this syntax implies that a switch and checkbox are the same thing, only visually different. That’s just not the case, though.
There are two core markup differences between the two inputs.
- A checkbox can be set to an indeterminate state — a switch cannot.
- A switch can be marked as required — a checkbox cannot.
Since both controls can be created with the same input, it’s inevitable that these attributes will become mixed and work unexpectedly.
The differences don’t stop at the markup conventions, though. How assistive technologies communicate checkboxes and switches is different as well. The state of checkboxes is communicated as “checked” or “unchecked”. Meanwhile, a switch is communicated as “on” or “off”. This may not seem like a big difference — and perhaps it isn’t — but grouping these two controls together leaves room for ambiguity. This is evident in our styling when targeting “on” switches with the :checked
pseudo selector. It’s a subtle distinction, but a distinction nonetheless.
Switches and checkboxes are similar, yes, but is coupling these two controls the right approach?
Accessibility (A11y): Understanding The Differences
Whenever we’d style over a checkbox to create a switch, we (hopefully) also attach role="switch"
to the component. Doing this has traditionally allowed assistive technologies to better understand the control. So, how is the new switch control interpreted by assistive technologies?
In this video, we learn how VoiceOver communicates the switch control while using Safari on Mac. But VoiceOver is just one — and not exactly the most representative — screen reader, and testing can and should be done with others.
Adrian Roselli has already published a round of thorough testing of different combinations of browsers and screen readers from 2022, but maintained with platform updates along the way. Spoiler alert: Results are utterly mixed, and the switch should stay out of production at the moment. Adrian offers deeper context:
“For more context, a switch element was proposed with WHATWG HTML in 2018, Google worked on it for a while, along with the some WICG discussion, then abandoned it. Open-UI started a discussion in 2021, but other than a comment on valid indeterminate use cases, has mostly died there.
Then in July 2023 […] a new contributor filed a WHATWG PR to add aswitch
attribute to a checkbox and here we are. Note that it has not been merged into WHATWG HTML, so Apple Safari has chosen to get way ahead of this one.”
— Adrian Roselli
Curiously, while VoiceOver announces the switch as either “on” or “off,” if we inspect the switch and review its accessibility properties in Safari’s developer tools, we will still see a reference to “checked.”
Communication is one aspect of the control’s accessibility. Earlier in 2024, there were issues where the switch would not adjust to page zoom levels properly, leading to poor or broken visibility of the control. However, at the time I am writing this, Safari looks to have resolved these issues. Zooming retains the visual cohesion of the switch.
The switch
attribute seems to take accessibility needs into consideration. However, this doesn’t prevent us from using it in inaccessible and unusable ways. As mentioned, mixing the required
and indeterminate
properties between switches and checkboxes can cause unexpected behavior for people trying to navigate the controls. Once again, Adrian sums things up nicely:
“Theswitch
role does not allow mixed states. Ensure your switch never gets set to a mixed state; otherwise, well, problems.”
— Adrian Roselli
Internationalization (I18N): Which Way Is On?
Beyond the accessibility of the switch control, what happens when the switch interacts with different writing modes?
When creating the switch, we had to ensure the use of logical CSS to support different writing modes and directions. This is because a switch being in its right-most position (or inline ending edge) doesn’t mean “on” in some environments. In some languages — e.g., those that are written right-to-left — the left-most position (or inline starting edge) on the switch would likely imply the “on” state.
While we should be writing logical CSS by default now, the new switch control removes that need. This is because the control will adapt to its nearest writing-mode
and direction
properties. This means that in left-to-right environments, the switch’s right-most position will be its “on” state, and in right-to-left environments, its left-most position will be the “on” state.
Final Thoughts
As we continue to push the web forward, it’s natural for our tools to evolve alongside us. The switch control is a welcome addition to HTML for eliminating the checkbox hacks we’ve been resorting to for years.
That said, combining the checkbox and switch into a single input, while being convenient, does raise some concerns about potential markup combinations. Despite this, I believe this can ultimately be resolved with linters or by the browsers themselves under the hood.
Ultimately, having a native approach to switch components can make the accessibility and usability of the control far more consistent — assuming it’s ever supported and adopted for widespread use.