Under-Engineered Toggles Too
Updated Intro
Whether you use a <button>
or <input type="checkbox">
for your toggle depends on a few factors:
- Use
<button>
if:- you can count on JavaScript being available,
- flipping the toggle has an immediate effect,
- the toggle will never have an indeterminate state.
Continue reading this post.
- Use
<input type="checkbox">
if:- you want to progressively enhance the control,
- flipping the toggle will only take effect when the user submits it,
- the toggle may have an indeterminate state.
Go read Under-Engineered Toggles.
Back to the Post
In my post Under-Engineered Toggles I showed you how to take a standard checkbox and use some CSS to style it to visually look like a toggle. That styling does nothing to change the semantics, nor should it. A checkbox is the default fallback for a switch
role. It also embraces Progressive Enhancement by not requiring any JavaScript.
However, there are cases where it makes sense to use a <button>
for a toggle instead, leaning on ARIA to create something like this: <button aria-pressed="true">
Generally I recommend against a control with a switch
role. Scott O’Hara has tracked a bunch of issues with screen readers, so you can see that the experience is sub-par overall.
Example
This pen shows the default <button>
from your browser and also shows the styled <button>
. This simple example does not show all the possible states and features that you should support. You may immediately notice that it does not look exactly like the iOS toggle people expect, primarily because some accessibility and usability best practices have been applied. I cover all those below and at the end of this piece is an example showing everything wrapped up together in a set of toggles.
See the Pen Under-Engineered Toggles Too: Comparison by Adrian Roselli (@aardrian) on CodePen.
Visit the example pen directly if it does not load.
Why Use a <button>
?
For a <button>
to act as a toggle, you will have to flip the value of aria-pressed
from true
to false
and vice versa.
If you are 100% confident that script will be available on the page, or you know the entire page will fall down, catch fire, and roll down a flight of stairs without JavaScript, then Progressive Enhancement is slightly less of a concern. This is import because without the ability to toggle the aria-pressed
attribute, this control will not work.
If flipping the toggle immediately performs an action on the page (such as updating a setting), then a <button>
is a better fit. Unlike a checkbox, which only changes its state, a <button>
conveys to a user that something will happen.
If the control will never have an indeterminate state, then <button>
with aria-pressed
may be right for you. It accepts true
or false
as well as mixed
. Granted, even if you want an indeterminate state, indeterminate (mixed
) is disallowed in a switch
role. The browser should ignore it on a checkbox cast as a switch
, but the styles won’t unless you adjust them. With the <button>
it won’t be an issue.
The
mixed
value is invalid, and user agents MUST treat amixed
value as equivalent tofalse
for this role.
Styles
The styles are generally the same as the checkbox version (see them at Under-Engineered Toggles). They account for focus and hover, disabled state, right-aligned controls, reduced motion, Windows High Contrast Mode, dark color scheme, and right-to-left text.
There are some default styles to reset the browser <button>
styles:
.toggles [aria-pressed] {
display: block;
box-sizing: border-box;
border: none;
color: inherit;
background: none;
font: inherit;
line-height: inherit;
text-align: left;
padding: .4em 0 .4em 4em;
/* position: relative; */
}
I left a commented-out instance of position: relative
in there because you may need it if you take the toggles out of these containers and use them elsewhere as a stand-alone.
You can see that I lean on [aria-pressed]
as a selector a lot (as well as [aria-pressed=true|false]
) throughout the styles. This helps ensure that I am capturing the correct <button>
s and that the visual styles match the programmatic state.
Script
The function to change the value of aria-pressed
is straightforward. You can probably make one that is more efficient, but this gets the job done.
function toggle(btnID) {
var theButton = document.getElementById(btnID);
if (theButton.getAttribute("aria-pressed") == "false") {
theButton.setAttribute("aria-pressed", "true");
} else {
theButton.setAttribute("aria-pressed", "false");
}
}
It is also a small enough function that you can embed it right in the page instead of offloading it to a linked file. Embedding it can address one aspect of Progressive Enhancement (the network dying).
Wrap-up
Paired with my previous post, Under-Engineered Toggles, we have two methods to create (mostly) binary (on/off) controls.
When we pull all that code together we can have a robust set of toggle styles that can adapt to user preferences for text size, contrast, language, motion, and interaction mode. As more features become available to us to honor user preferences and platform features, then we can fold those in as well.
See the Pen Under-Engineered Toggles Too by Adrian Roselli (@aardrian) on CodePen.
Recap
What this post covered:
- Pointing you to the first post in this set, Under-Engineered Toggles,
- Reasons you might prefer a
<button>
to a checkbox, - Styles unique to the
<button>
, - The script to toggle the
aria-pressed
attribute.
Talk Reference (Added 7 July 2024)
This post was mentioned at WordPress Accessibility Day 2023.
YouTube: Creating an Animation Pause Button in WordPress by by Danielle Zarcaro.
2 Comments
I’m implementing this pattern for a no-JS form with code similar to the following:
<form method="post">
<!-- Default submit for the enter button, if you include the password toggle. -->
<button hidden></button><p><label>Username/email address:
<input name="user"
autocomplete="username"
autocapitalize="off" autocorrect="off" spellcheck="off"
value="<?php echo $_GET['user']?>"
required>
</label></p><p><label>Password:
<input type="<?php echo $_GET['show-password'] ? 'text': 'password'?>"
name="password"
autocomplete="current-password"
minlength="12"
passwordrules="allowed:unicode"
value="<?php echo $_GET['password']?>"
required>
</label><button formnovalidate name="show-password"
value="<?php echo $_GET['show-password'] ? '': 'on'?>"
aria-pressed="<?php echo $_GET['show-password'] ? 'false': 'true'?>">
<?php echo $_GET['show-password'] ? 'Hide': 'Show'?> password
</button>
</p><p><button>Log in</button></p>
</form>
Am I implementing the pattern correctly, or is there something I’m missing?
In response to .Taylor, if I understand your question correctly, as a no-JS form that means there is no client-side script or you are treating it as if there is none.
If the button (toggle) submits the form and you handle its
aria-pressed
value server-side, then it is a more correct choice than a checkbox. Primarily because a user expects a password toggle to change a part of the page immediately.If JS is available then I would expect the JS does the work client-side, and the server-side processing is the fallback.
Be careful with changing the button text (forget the
value
attribute). If the button text changes from “Show password” to “Hide password” and back, then a user will not understand which state is which. Choose the text and stick with it; let thearia-pressed
/ visual style convey whether it is active or not.
Leave a Comment or Response