DEV Community

Cover image for Accessible Toggle
Paweł Ludwiczak
Paweł Ludwiczak

Posted on

Accessible Toggle

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;
    }

    ...
}
Enter fullscreen mode Exit fullscreen mode

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;
        ...
    }

    ...
}
Enter fullscreen mode Exit fullscreen mode

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).

Discussion (12)

Collapse
afif profile image
Temani Afif

If you want, you can achieve the same using only the native input element

Collapse
pp profile image
Paweł Ludwiczak Author

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:)

Collapse
afif profile image
Temani Afif

that issue is for sure related to the aspect-ratio. Replace it with the classic width/height and it should work fine ;)

Collapse
jameslivesey profile image
James Livesey

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.

Collapse
pp profile image
Paweł Ludwiczak Author

If I understand this correctly, it would require JS to change the aria-checked value (which is required when using role="switch" and btw that seems to be missing in the Adapt UI Demo you linked to) and that was a constraint in my example.

Collapse
jameslivesey profile image
James Livesey • Edited on

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!

Collapse
moopet profile image
Ben Sinclair

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:

a checkbox styled to look like a toggle, with clear "yes" and "no" states, and a label reading "auto-renew"

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).

Collapse
pp profile image
Paweł Ludwiczak Author

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):

<label>
    <span class="actual-label">Auto-renew</span>
    <span class="actual-toggle">[checkbox etc.]</span>
</label>
Enter fullscreen mode Exit fullscreen mode

^ 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:

<label>
    <Description />
    <Toggle />
</label>
Enter fullscreen mode Exit fullscreen mode

^ 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:

  1. I think iconography would make a bit more sense here (✔︎ vs ✘) because of localization - in some languages "YES" and "NO" can be longer affecting the layout of a toggle element.
  2. I'm not sure (I genuinely don't know that and need to google a bit) if "YES" and "NO" should be part of a <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 :).
Collapse
moopet profile image
Ben Sinclair

I think I misunderstood when you said:

Keep in mind, all form elements should be placed inside label, but [...]

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 :)

Collapse
ben profile image
Ben Halpern

Great writeup

Collapse
supportic profile image
Supportic

This is also a good addition: codepen.io/KittyGiraudel/pen/xxgrPvg

Collapse
vulcanwm profile image
Medea

Nice explaining!