Intro
Toggle is a TRUE/FALSE switch that looks like a... switch... Similar control you can find in your smartphone settings probably.
Under the hood, since it's a TRUE/FALSE type of a control, it's basically just a more fancy styled input[type="checkbox"]
.
And here's why it's tricky to make it accessible.
Usual way
Usually, to build such a toggle with HTML & CSS you need a visually hidden checkbox within a fancy styled label
element. And that's what most of the tutorials will tell you.
label
element makes sure that the entire toggle is "interactive" (i.e. clickable).
You can't just hide the checkbox with display: none
because it would make it invisible to assistive technologies. So here's the trick to make it invisible to screens yet accessible:
label.toggle {
position: relative;
...
[type="checkbox"] {
position: absolute;
left: -10000px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
...
}
The Problem and The Solution
And here's a caveat: our toggle (which is a checkbox) is wrapped with label
therefore it's required (by the a11y police 👮) to provide an actual text description. But we don't want our component to dictate how that text description should look like... Because it might be used in several different ways:
- sometimes text might be in front of a toggle, sometimes after it, and sometimes above it,
- sometimes text might be longer and styled and sometimes not.
Hence we can't use label
🤷♂️. But if we use regular div
or span
instead, then we lose the "clickability" of our toggle.
So the actual trick is to enlarge the checkbox to be as big as the toggle wrapper (that div
or span
) and hide it differently:
div.toggle {
position: relative;
...
[type="checkbox"] {
width: 100%;
height: 100%;
opacity: 0.00001;
...
}
...
}
First we make sure checkbox takes full width and height. Then we almost hide the checkbox with opacity: 0.00001;
.
Opacity can take values between 0
(fully invisible) and 1
(fully visible). We use value very close to 0 so it's basically invisible on the screen but visible to assistive technologies.
That's it folks. Below is a full demo :)
^ try to remove that opacity property to see the actual checkbox placement.
Couple notes:
- You could practically use any element to create toggle if you also sprinkle it with JS. But I wanted to avoid it.
- Keep in mind, all form elements should be placed inside
label
, but in the World of Components, we don't really want such a low-level component to dictate how everything around it should look like. Components should be self-contained and not affect its surroundings (at least that's my philosophy).
Top comments (13)
If you want, you can achieve the same using only the native input element
Oh this is lovely proof of concept! I think it needs a bit polishing (I’m checking it on my mobile and there’s something off with proportions but I’m pretty sure it’s easily fixable) but I love it! The simpler, the better as long as it’s bullet proof 👌 Anyway, I may steal this one from you:)
that issue is for sure related to the aspect-ratio. Replace it with the classic width/height and it should work fine ;)
Why would you want to avoid using a label? It's the right element to use when you want to... label something, and it makes no sense to have a switch without context.
Interestingly, I got tasked with making a toggle like this recently, and the UX called for it to be "default on". The design first came through with it active on when the switch was to the left, which I had to question, and got changed to the more conventional right-side-on pattern.
The front-end person building the other components on the project happens to be colourblind, and they saw the original as "it's on the left, and it's one shade of grey-brown, and the grey-brown changes slightly when you move it right". They would have used the control wrong in the real world.
My partner struggles with left vs right for meaning (and she's always telling me "take the next left, no the other left" when we're driving somewhere...) and I have another friend who's dyslexic and it manifests in not associating direction with intent like that.
We got a redesign, and I can't easily show you the CSS because I'd have to unpick it all, but that's irrelevant for the purposes of this comment. Here's a screenie:
It uses a label element and has clear, contextually-appropriate states (in this case "yes" and "no", but they could be changed depending on the proposition the label makes).
First of all, thank you for this feedback, I really appreciate it! Let me address some thing below:
As I mentioned at the end of my post: I do think all controls should have label associated - no doubts about that. But this whole thing came up because we didn't want descriptive label to be part of the control itself in our design system. We try our base components to be as low-level as possible. It's just how our design system is built. And that's because in our designs we use toggles in few different visual configurations: sometimes they are above label, sometimes they are below label, sometimes they are on the left of it and sometimes on the right.. Sometimes labels are super descriptive with additional helper texts and sometimes they are very short. Your example above with "Auto-renew" layout is just one of the possible use cases.
So on one hand we could have this (which I believe is your approach from screenshot):
^ but again, we don't want our design system components to define the layout of entire element because then we would need that "Auto-renew" to be part of the component itself (so the entire
<label>
would have to be a component. Hence this approach would be very limiting to us.Instead we prefer to be more granular:
^ so the
<Description />
can be anything we want and at the same time we have full control over<Toggle />
placement and so the general<label>
's layout.HOWEVER you also brought up very good point about folks with some sighting issues and other disabilities and also displaying the value "YES/NO". I do think it's important to add that to the UI control but:
<label>
element itself as I always thought<label>s
are meant to describe the field purpose and not its value. But again - that's something I'd have to verify :).I think I misunderstood when you said:
To mean that you knew what you should do and were choosing not to, rather than that you would wrap the whole low-level component in a label.
And yes, we'd need to do localisation depending on use-case :)
Even better is to use
aria-role="switch"
— not only does it tell screen readers that the checkbox is more specifically one that resembles a switch*, but it also gives you an extra way to style checkboxes without having to use a seperate class! We used it for Adapt UI's switch element, as shown at the bottom of our demo.Additionally, it's possible to style the
input
element itself instead of having to rely on an adjacent element to render the design of the switch —appearance: none;
always comes in handy!*Not to mention that the difference between a switch and a checkbox in terms of UX standards is that when a switch is changed, its state is immediately applied, but when a checkbox is changed, it usually requires confirmation using a seperate button.
If I understand this correctly, it would require JS to change the
aria-checked
value (which is required when usingrole="switch"
and btw that seems to be missing in the Adapt UI Demo you linked to) and that was a constraint in my example.Good point — that's definitely a thing to add to our library! Having tested our demo in a few screen readers, they seem to still be okay with using checkboxes'
checked
value with the role, but it's a good idea to add the extra attribute (with JS or course) for assistive technologies that aren't as good at detecting that. Thanks for the advice!I would assume from the docs that the
aria-checked
attribute is used mainly so screen readers can tell the user when a checkbox changes state on its own (without interaction), which would make sense!Great writeup
This is also a good addition: codepen.io/KittyGiraudel/pen/xxgrPvg
Nice explaining!
Very helpful ! Thank you for this !