Toggling elements accessibly

Toggling is the act of showing or hiding an element. We usually do this via the user performing an action on a button element.

It’s important to hide elements correctly to ensure screen-readers and input devices (i.e. a keyboard) cannot access them (unless you want them to) and also inform screen-readers that the button they have in focus is a toggle button and what its current state is (i.e. is it 'expanded' or 'collapsed').

Basic example

This basic example has no animation so we can hide the element with the hidden attribute, using code like this:

<button aria-controls="terms" aria-expanded="false" type="button">
  Terms and conditions
</button>
<div id="terms" hidden>
  <h2>Terms</h2>
  <p>Terms and conditions will go here.</p>
  <p><a href="terms.html">Further terms</a></p>
</div>

and then when expanded it looks like this…

<button aria-controls="terms" aria-expanded="true" type="button">
  Terms and conditions
</button>
<div id="terms">
  <h2>Terms</h2>
  <p>Terms and conditions will go here.</p>
  <p><a href="terms.html">Further terms</a></p>
</div>

Animated example

This example hides the '#terms' element in a different way because it needs to animate it. Remember hidden acts like display: none and you cannot animate/transition with CSS from display:none. So we use the inert attribute to hide the content from screen-readers and prevents its focusable child elements from coming into focus via a keyboard’s TAB key stroke.

<button aria-controls="terms" aria-expanded="false" type="button">
  Terms and conditions
</button>
<div
  id="terms"
  inert
  style="overflow: hidden; maxHeight: 0; transition: max-height 1s"
>
  <h2>Terms</h2>
  <p>Terms and conditions will go here.</p>
  <p><a href="terms.html">Further terms</a></p>
</div>

then when expanded it looks like this…

<button aria-controls="terms" aria-expanded="true" type="button">
  Terms and conditions
</button>
<div
  id="terms"
  inert
  style="overflow: hidden; maxHeight: 500px; transition: max-height 1s"
>
  <h2>Terms</h2>
  <p>Terms and conditions will go here.</p>
  <p><a href="terms.html">Further terms</a></p>
</div>

If we do not/cannot use inert then we have to prevent the user from being able to TAB to any visually hidden child elements (like the a element inside the #terms div) and we do this by assigning interactive elements the attribute/value combo of tabindex="-1".

Like this:

<button aria-controls="terms" aria-expanded="false" type="button">
  Terms and conditions
</button>
<div
  aria-hidden="true"
  id="terms"
  inert
  style="overflow: hidden; maxHeight: 0; transition: max-height 1s"
>
  <h2>Terms</h2>
  <p>Terms and conditions will go here.</p>
  <p><a href="terms.html" tabindex="-1">Further terms</a></p>
</div>

then when expanded it looks like this…

<button aria-controls="terms" aria-expanded="true" type="button">
  Terms and conditions
</button>
<div
  id="terms"
  style="overflow: hidden; maxHeight: 500px; transition: max-height 1s"
>
  <h2>Terms</h2>
  <p>Terms and conditions will go here.</p>
  <p><a href="terms.html">Further terms</a></p>
</div>

Further reading

  1. aria-expanded MDN docs
  2. hidden MDN docs
  3. inert MDN docs
  4. Hiding Elements accessibly
  5. Scott O'Hara - A quick summary of tabindex values

Want to work with us?

Let’s talk about your project