DEV Community

loading...
Cover image for Expand the Content Inclusively - Building an Accessible Accordion with React

Expand the Content Inclusively - Building an Accessible Accordion with React

eevajonnapanula profile image Eevis (she/her) ・5 min read

Have you ever encountered a situation where a website acts unexpectedly? For instance, you are trying to click a link, but it actually is not a link (even though it looks like one)? Isn't that frustrating? A similar scenario might happen to many users if we, as developers, ignore keyboard accessibility and correct semantics.

I'm starting a series of blog posts about accessible React components. I hope to give you some tools for making a little more accessible web with this framework throughout the series.

WAI-ARIA Authoring practices offer Design patterns and widgets for making common patterns and widgets with keyboard interactivity and aria-attributes. As they write:

This section demonstrates how to make common rich internet application patterns and widgets accessible by applying WAI-ARIA roles, states, and properties and implementing keyboard support.

This is the first part of the blog posts series about creating Design patterns and widgets from WAI-ARIA authoring practices with React.
In this post, I will take a look at the accordion-pattern.

What is an Accordion?

An accordion is a "vertically stacked set of interactive headings that each contain a title, content snippet, or thumbnail representing a section of content" (WAI-ARIA Authoring practices). The title works as a control for hiding or showing the content. Here's an example accordion:

I'm the title of the accordion!

I'm the content, and I can hide.

There is indeed a semantic HTML element for when the accordion is simple enough. It is called details and is usable with most browsers, according to Can I Use?. However, sometimes we need more control over the accordion, and in those cases, we can build an accordion by giving it semantics with ARIA.

Roles, States, and Properties

There are several things to note about roles, states, and properties to make the accordion accessible. As I am creating only a minimal example, the following should be enough:

  • The heading of the accordion should have an element with the role button. Another tag with a role heading should wrap that button. In our case, this would mean <button> and <h2> elements, which are semantic tags.
  • The expanded-state should also be communicated to the screen reader user. It would mean an aria-expanded-attribute set to be true or false depending on if the panel is open.
  • The button should have an aria-controls-attribute set to point to the id of the accordion content. It communicates that "this button controls the element/content it points to" by pointing to the controlled area. (A note: It seems that this aria-attribute works only with JAWS and is even a bit problematic)

So, when using this as guidance, I can start building the accordion. First, I'll add the elements:

const Accordion = () => {
  return (
    <section>
      <div>
        <h2>
          <button>I'm the one opening the accordion!</button>
        </h2>
      </div>
      <div>
        <p>I'm the content, yay.</p>
      </div>
    </section>
  )
}
Enter fullscreen mode Exit fullscreen mode

Here I use the native h2 and button-elements, so they check the first item. I will also need the functionality for opening and closing the accordion. In this example, I'll use the useState-hook and use the variable's value to communicate if the accordion is open.

I use the hidden-attribute for hiding the content. It is controlled by the isOpen-state - if it is true, hidden is false, and vice versa. isOpen is also used for the aria-expanded-attribute in the button to communicate if the accordion is expanded:

const Accordion = () => {
  const [isOpen, setIsOpen] = useState(false)
  const handleVisibilityToggle = () => setIsOpen(!isOpen)
  return (
    <section>
      <div>
        <h2>
          <button 
            aria-expanded={isOpen}
            onClick={handleVisibilityToggle}
          >
            I'm the one opening the accordion!
          </button>
        </h2>
      </div>
      <div hidden={!isOpen}>
        <p>I'm the content, yay.</p>
      </div>
    </section>
  )
}
Enter fullscreen mode Exit fullscreen mode

This covers the second item on the list. The next thing to do is to add aria-controls to the button-element. For this, an id for the wrapper of the content is needed. The aria-controls-attribute should be set to that id:

const Accordion = () => {
  // ...
  return (
    <section>
      <div>
        <h2>
          <button 
            aria-expanded={isOpen}
            aria-controls="accordion-content"
            onClick={handleVisibilityToggle}
          >
            I'm the one opening the accordion!
          </button>
        </h2>
      </div>
      <div
        id="accordion-content" 
        hidden={!isOpen}
       >
        <p>I'm the content, yay.</p>
      </div>
    </section>
  )
}
Enter fullscreen mode Exit fullscreen mode

Now that the required semantics are added, it is time to ensure that the accordion can be used with a keyboard.

Keyboard Interaction

There are some keyboard interaction patterns required to work with the accordion:

  • Enter or space: Used to open or close the accordion.
  • Tab: Used to navigate from one focusable item to the next.
  • Shift + Tab: Used to navigate from one focusable item to the previous one.

In addition to these, there are four more optional keyboard shortcuts. If you're interested, they are explained in the WAI-ARIA Authoring practices.

Looking at the elements used for the structure (mainly the button-element), these keyboard interactions are there if the semantic elements are used. The button has built-in support for activating with enter and space. Also, as it is an interactive control, it is focusable by default. This means, that tab and shift + tab work out of the box. Well, unless you use Mac and Safari and haven't enabled keyboard accessibility.

Other Accessibility Considerations

In the previous two sections, I have covered how to make the accordion accessible for people who benefit from keyboard accessibility and aria roles, states, and properties. To make the accordion accessible for all users, there are other things to consider as well.

I've left the accordion styles out from this post and will only mention that it is essential to use accessible color combinations for the accordion. For example, this means that the contrast ratio should be 4.5:1 for text and 3:1 for large text on AA-level, and 7:1 and 4.5:1 for large text on AAA-level. If you wonder what I mean by the levels, they're levels of WCAG Success Criteria, and they're explained in WCAG's documentation.

When styling anything in the website, also keep in mind that some users are using the Windows High Contrast Mode, which modifies the site's colors. If you have never heard about Windows High Contrast Mode, I wrote a blog post about it a couple of weeks ago.

Apart from the color and styles, the accordion's content needs to be taken into account. What it actually means depends on the content: If you have texts, they should be written in simple language; if you have images, they should have meaningful alt-texts, and so forth.

Wrap-up

In this blog post, I've explained one way how to build an accessible accordion with React and how to add keyboard accessibility and aria-roles, states, and properties to it. This has been done according to the WAI-ARIA Authoring practices' Design Patterns, which has many different custom widgets with required keyboard shortcuts and aria-roles, states, and properties.

You can see an example-accordion (with the optional aria-roles, states and properties, and keyboard shortcuts in place) in a site I created for showing the complete code for these blog posts. Here's also a direct link to the source code of the accordion component.

If you have any questions or comments, I'll be happy to answer! 😊

Discussion (0)

pic
Editor guide