A11y Toggle

Get the code on GitHub or skip to a specific documentation section right away:

Pure CSS toggles using the checkbox hack introduce some usability and accessibility problems.

For starters, the checkbox hack relies on the :checked pseudo-class, which is not supported everywhere. For instance, BlackBerry browser, Opera Mini, Android Stock Browser and Firefox Android (amongst others) do not support this selector, making the whole thing broken.

On top of that, in order to be fully accessible a content toggle needs some extra attributes that cannot be toggled without JavaScript. The toggle itself needs a aria-controls attribute linking to the expandable element, and a aria-expanded attribute describing its state (expanded or collapsed). The collapsed element itself needs the aria-hidden attribute when invisible.

a11y-toggle takes care of all these considerations for you. Initial necessary attributes are being added by the script so you don’t even have to care about this.

No more excuse now. Make your toggles accessible.

Getting started

To get started, simply include a11y-toggle.js (ideally the minified version, of course) in your document. It can be either in the <head> or at the bottom of the <body> although the latter the better, for performance reasons.


<script src="a11y-toggle.min.js" async></script>
          

That’s it! There is no need to initialize anything as everything else is being handled by the script itself. Simply check the next section to see how to declare your toggles.

Basic example

The bare minimum is a button with the data-a11y-toggle attribute mapped to an existing id in the document. You can have a look at the code below.


<button type="button" data-a11y-toggle="target">
  Toggle code »
</button>

<div id="target">
  Some content…
</div>
          

Initial ARIA-specific attributes such as aria-expanded, aria-hidden and aria-labelledby, as well as id on the toggle element (if none already) are being added automatically.

On the CSS Side, only hiding aria-hidden content is strictly necessary. It is also recommended to hide the button when JavaScript is disabled, by checking the presence of the aria-controls attribute for instance.


[aria-hidden='true'],
[data-a11y-toggle]:not([aria-controls]) {
  display: none;
}
          

Note: globally hiding elements with [aria-hidden="true"] can have rendering issues with third party integrations. Only known to date is with Google Maps, in which it prevents the map tiles to render. Therefore it needs to be resetted inside a Google Maps container.

See the Pen a11y-toggle — Basic example by Hugo Giraudel (@HugoGiraudel) on CodePen.

Expanded by default

If you want the content to be expanded by default, you can add a data-a11y-toggle-open attribute to the target element (not the toggle itself).

Note that this is the default behaviour when JavaScript is disabled, so that the content remains accessible even without JavaScript.


<button type="button" data-a11y-toggle="target">
  Toggle code »
</button>

<div id="target" data-a11y-toggle-open>
  Some content that is expanded by default…
</div>
          

See the Pen a11y-toggle — Expanded by default by Hugo Giraudel (@HugoGiraudel) on CodePen.

Non-button toggles

A <button> element is definitely the most suited one for a toggle, however sometimes it might not be possible for whatever reason.

It is possible to use any other element as a toggle however you must manually specify the following attributes:

  • role="button"
  • tabindex="0"

<span role="button" tabindex="0" data-a11y-toggle="target">
  Toggle code »
</span>

<div id="target">
  Some content…
</div>
          

Because of a nasty bug on iOS, you have to add this rule to your stylesheet to make the toggle actually clickable. Apparently, no pointer means no events.


[role="button"] {
  cursor: pointer;
} 
          

See the Pen a11y-toggle — Non-button toggles by Hugo Giraudel (@HugoGiraudel) on CodePen.

Multi toggles

A collapsible container can have several toggles able to control its visibility. There is no difference in markup regarding multi toggles.


<button type="button" data-a11y-toggle="target">
  Toggle code »
</button>

<button type="button" data-a11y-toggle="target">
  Toggle code »
</button>

<div id="target">
  Some content…
</div>
          

See the Pen a11y-toggle — Multi toggles by Hugo Giraudel (@HugoGiraudel) on CodePen.

Animations

Given that a11y-toggle is completely unopinionated regarding the styling layer, it is really up to the developer to implete the sliding and/or fading animation. Here is an example below. Hat tip to Nicolas Hoffman.

The markup does not change compared to the original version except that we add a class to the collapsible sections to be able to target them in CSS.


<button type="button" data-a11y-toggle="target">
  Toggle code »
</button>

<div class="collapsible-box" id="target">
  Some content…
</div>
          

The styles do not rely on display: none anymore but a tricky / hacky combination of declarations to make the magic happen.


.collapsible-box {
  overflow: hidden;
  opacity: 1;
  max-height: 80em;
  visibility: visible;
  transition:
    visibility 0s ease,
    max-height 2s ease,
    opacity    2s ease;
  transition-delay: 0s;
}

.collapsible-box[aria-hidden='true'] {
  max-height: 0;
  opacity: 0;
  visibility: hidden;
  transition-delay: 2s, 0s, 0s;
}
          

See the Pen a11y-toggle — Animations by Hugo Giraudel (@HugoGiraudel) on CodePen.

Connected toggles

The library does not provide out-of-the-box support for connected toggles, which are a set of toggles with only one expanded at any time.

However with a tiny bit of JavaScript, it is possible to leverage the capabilities of the library to add this feature.


<div class="connected-toggles">
  <button data-a11y-toggle="target-1" type="button">
    Toggle content 1 »
  </button>

  <button data-a11y-toggle="target-2" type="button">
    Toggle content 2 »
  </button>

  <div id="target-1">
    Some content (1)… 
  </div>
  
  <div id="target-2">
    Some content (2)…
  </div>
</div>
          

The trick is to collapse all targets in the set when one is being activated, except this one. The JavaScript snippet to achieve that should be pretty straight-forward.


function collapse (toggle) {
  var id = toggle.getAttribute('data-a11y-toggle');
  var collapsibleBox = document.getElementById(id);
  collapsibleBox.setAttribute('aria-hidden', true);
  toggle.setAttribute('aria-expanded', false);
}

function collapseAll (event) {
  toggles
    .filter(function (toggle) {
      return toggle !== event.target;
    })
    .forEach(collapse);
}

var toggles = Array.prototype.slice.call(
  document.querySelectorAll('.connected-toggles [data-a11y-toggle]')
);

toggles.forEach(function (toggle) {
  toggle.addEventListener('click', collapseAll);
});
          

Note that if you have several instances of connected toggles on your page, you might want to read this issue, as the current code is too simple to cover this edge case.

Beware! a11y-toggle is not a library to build tabs. While it certainly is possible to do so, remember that tabs imply other accessibility concerns which should be taken care of.

See the Pen a11y-toggle — Connected toggles by Hugo Giraudel (@HugoGiraudel) on CodePen.

Dynamically injected toggles

a11y-toggle fires once when the DOM is fully loaded (DOMContentLoaded). You will have to relaunch it if you dynamically add new toggles with JavaScript.

Thankfully, a11y-toggle exposes an a11yToggle function on the global object.


window.a11yToggle();
          

If you do not want to reinitialise everything, you can pass a context to the a11yToggle function which will be used as a root for .querySelectorAll.


var newContainer = document.getElementById('new-container');
window.a11yToggle(newContainer);