Under-Engineered Toggles Too

Updated Intro

Whether you use a <button> or <input type="checkbox"> for your toggle depends on a few factors:

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

  2. 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 a mixed value as equivalent to false 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.

Visit this pen directly.

Recap

What this post covered:

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

Reply

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 Hunt. Reply

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 the aria-pressed / visual style convey whether it is active or not.

Leave a Comment or Response

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>