iTranslated by AI
Thoughts on Implementing Accessible Accordions
Accordion UI is often seen in Q&A sections and similar interfaces, but it can be surprisingly complicated to implement when considering accessibility.
The purpose of this article is to explore accordion implementation with reference to the ARIA Authoring Practices Guide (APG).
The ARIA APG page regarding accordions can be found here:
What is an Accordion in the First Place?
First of all, what kind of UI is the accordion discussed in this article?
Let's look at the About This Pattern section.
- It is a set of vertically stacked interactive headings.
- It consists of a header, which is the heading, and a panel, which is the content.
- The header contains a title (and potentially content snippets or thumbnails) representing the section of content.
- The header also functions as a control for opening and closing the panel.
- It is often used to reduce scrolling when displaying multiple sections on a single page.
There are other points as well, but I think the above provides a rough summary.
Implementation
Now that we have a rough idea of what an accordion UI is, let's move on to the implementation.
Base Coding
For now, let's try implementing it without worrying too much about the details.
While it is a very simple implementation where the panel just opens and closes when the header is clicked, it has the minimum functionality required.
We will proceed with the discussion using this implementation as a base.
<p class="accordion-header">
<!-- Get the corresponding panel id via data-panel -->
<!-- Change icon orientation with .__open -->
<span class="accordion-trigger" data-panel="accordion-panel-1">
Accordion Heading 01<span class="accordion-icon"></span>
</span>
</p>
<!-- Open/close panel with .__close -->
<div id="accordion-panel-1" class="accordion-panel __close">
<p class="accordion-panel__text">
Accordion content. Accordion content. Accordion content.
</p>
</div>
/* Change icon orientation with .__open */
.accordion-trigger.__open .accordion-icon {
transform: rotate(-45deg);
}
/* Open/close panel with .__close */
.accordion-panel.__close {
display: none;
}
triggers.forEach((trigger) => {
// Get the corresponding panel
const dataPanel = trigger.dataset.panel;
const panel = document.getElementById(dataPanel);
trigger.addEventListener("click", (e) => {
// Get the open/closed state
const target = e.currentTarget;
const isOpen = trigger.classList.contains("__open");
if (isOpen) {
// Close the panel
target.classList.remove("__open");
panel.classList.add("__close");
} else {
// Open the panel
target.classList.add("__open");
panel.classList.remove("__close");
}
});
});
Adjusting HTML Tags
Now, let's start thinking about implementing an accessible accordion, beginning with the markup structure and the selection of appropriate tags. While the ARIA APG includes explanations about keyboard interaction and ARIA attributes, we'll start here because proper markup can prevent unnecessary implementation work.
WAI-ARIA Roles, States, and Properties contains the following description:
- The title of each accordion header is contained in an element with role button.
- Each accordion header button is wrapped in an element with role heading that has a value set for aria-level that is appropriate for the information architecture of the page.
- If the native host language has an element with an implicit heading and aria-level, such as an HTML heading tag, a native host language element may be used.
- The button element is the only element inside the heading element. That is, if there are other visually persistent elements, they are not included inside the heading element.
Based on the above, I will use the following markup in this article:
- Include the accordion title within a
button. - Wrap the
buttonin a heading tag (htag).
The main code changes are as follows:
- <p class="accordion-header">
- <span class="accordion-trigger" data-panel="accordion-panel-1">
- Accordion Heading 01<span class="accordion-icon"></span>
- </span>
- </p>
+ <h2 class="accordion-header">
+ <button class="accordion-trigger" data-panel="accordion-panel-1">
+ Accordion Heading 01<span class="accordion-icon"></span>
+ </button>
+ </h2>
<div id="accordion-panel-1" class="accordion-panel __close">
<p class="accordion-panel__text">
Accordion content. Accordion content. Accordion content.
</p>
</div>
Here is the complete code:
Keyboard Interaction
Next, let's consider keyboard interaction.
Let's take a look at Keyboard Interaction.
EnterorSpace:
- When focus is on the accordion header for a collapsed panel, expands the associated panel. If the implementation allows only one panel to be expanded, and if another panel is expanded, collapses that panel.
- When focus is on the accordion header for an expanded panel, collapses the panel if the implementation supports collapsing. Some implementations require one panel to be expanded at all times and allow only one panel to be expanded; so, they do not support a collapse function.
Tab: Moves focus to the next focusable element; all focusable elements in the accordion are included in the page Tab sequence.Shift + Tab: Moves focus to the previous focusable element; all focusable elements in the accordion are included in the page Tab sequence.
While the ARIA APG also explains optional features (related to arrow keys), we will not implement them this time.
Furthermore, regarding accordion behavior, there are patterns where only one panel is expanded at a time and patterns where multiple panels can be expanded simultaneously. This article assumes the latter (the base implementation is also structured this way).
Therefore, based on the above, the keyboard interaction will be as follows:
- When
EnterorSpaceis pressed while the header has focus, the panel opens if it is closed and closes if it is open. - When
Tabis pressed, the focus moves to the next focusable element. - When
Shift + Tabis pressed, the focus moves to the previous focusable element.
In fact, the above behaviors have already been addressed in Adjusting HTML Tags.
We can see how proper markup can prevent unnecessary implementation work.
WAI-ARIA Implementation
Finally, let's consider the implementation of WAI-ARIA.
WAI-ARIA Roles, States, and Properties also contains the following description:
- If the accordion panel associated with an accordion header is visible, the header button element has aria-expanded set to true. If the panel is not visible, aria-expanded is set to false.
- The accordion header button element has aria-controls set to the ID of the element containing the accordion panel content
Based on the above, we will implement it as follows:
- Set aria-expanded on the header's
button. Set it totruewhen the associated panel is open andfalsewhen it is closed. - Add aria-controls to the header's
button, specifying the id attribute of the associated panel.
Although not covered in this article, it is possible in some cases to assign a region role to the panel (or use the section tag). In that case, use aria-labelledby to reference the button that controls the opening and closing of the panel. Please refer to the ARIA APG for details.
The following is the main revised code:
<h2 class="accordion-header">
<button
class="accordion-trigger"
- data-panel="accordion-panel-1"
+ aria-controls="accordion-panel-1"
+ aria-expanded="false"
>
Accordion Heading 01<span class="accordion-icon"></span>
</button>
</h2>
<div id="accordion-panel-1" class="accordion-panel __close">
<p class="accordion-panel__text">
Accordion content. Accordion content. Accordion content.
</p>
</div>
- .accordion-trigger.__open .accordion-icon {
+ .accordion-trigger[aria-expanded="true"] .accordion-icon {
transform: rotate(-45deg);
}
triggers.forEach((trigger) => {
- const dataPanel = trigger.dataset.panel;
- const panel = document.getElementById(dataPanel);
+ const controls = trigger.getAttribute("aria-controls");
+ const panel = document.getElementById(controls);
trigger.addEventListener("click", (e) => {
const target = e.currentTarget;
- const isOpen = target.getAttribute("aria-expanded") === "true";
+ const isOpen = trigger.classList.contains("__open");
if (isOpen) {
- target.classList.remove("__open");
+ target.setAttribute("aria-expanded", "false");
panel.classList.add("__close");
} else {
- target.classList.add("__open");
+ target.setAttribute("aria-expanded", "true");
panel.classList.remove("__close");
}
});
});
Here is the complete code:
Good job! That's it for the implementation.
Bonus: About details/summary
Although not mentioned in the ARIA APG accordion section, using details / summary allows you to implement a similar UI (though not a complete replacement) without being overly concerned with the various points we've discussed so far.
On the other hand, there seems to be a lot of debate as to whether this UI should be called an accordion, and the following articles were very helpful:
It is worth noting that, currently, nesting an h tag inside a summary may neutralize the heading role of the h tag.
In fact, when I checked with VoiceOver on my Mac, I found that it did not announce the "Heading level XX" part.
For these reasons, some argue that as a UI in ARIA APG, it may be closer to a Disclosure than an accordion.
Bonus Part 2: About hidden="until-found"
I recently learned about hidden="until-found", so I am adding an update here.
While not mentioned earlier, one advantage of using details/summary is that the accordion opens when a search term matches during an in-page search.
As far as I've checked, there is no mention of in-page search in the ARIA APG's accordion section. Looking at the Accordion Example, hidden words aren't discovered during an in-page search if the accordion is collapsed.
By using hidden="until-found", you can enable finding hidden words even when the accordion is collapsed, and have the accordion open automatically.
First, let's try implementing it.
Try searching for the phrase "hits when searched" (検索するとヒットします。) using your browser's in-page search. You should see that the word is found even when the accordion is collapsed, and the accordion opens automatically.
Let's take a look at the code.
First, add hidden="until-found" to the panel part in the HTML.
<div
id="accordion-panel-1"
- class="accordion-panel __close"
+ class="accordion-panel"
+ hidden="until-found"
>
<p class="accordion-panel__text">
Accordion content. Accordion content. Accordion content.
</p>
</div>
Elements with the hidden="until-found" attribute have the content-visibility: hidden style applied. If you hide the elements with display: none, they won't show up in searches, so we will correct that implementation here.
- .accordion-panel.__close {
- display: none;
- }
The beforematch event can be used on elements with hidden="until-found". For more details, please refer to MDN. The beforematch event is triggered when a hidden element within hidden="until-found" is found during an in-page search.
In the code below, we remove the hidden attribute when the beforematch event is received. Also, don't forget to handle the aria attributes to maintain consistency.
triggers.forEach((trigger) => {
const controls = trigger.getAttribute("aria-controls");
const panel = document.getElementById(controls);
trigger.addEventListener("click", (e) => {
const target = e.currentTarget;
const isOpen = target.getAttribute("aria-expanded") === "true";
if (isOpen) {
// Close the accordion
target.setAttribute("aria-expanded", "false");
- panel.classList.add("__close");
+ panel.setAttribute("hidden", "until-found");
} else {
// Open the accordion
target.setAttribute("aria-expanded", "true");
- panel.classList.remove("__close");
+ panel.removeAttribute("hidden");
}
});
+ // When a search matches within hidden="until-found"
+ panel.addEventListener("beforematch", (e) => {
+ const target = e.currentTarget;
+
+ // Open the accordion
+ target.removeAttribute("hidden");
+
+ // Update the trigger
+ const id = target.id;
+ const trigger = document.querySelector(`button[aria-controls="${id}"]`);
+ trigger.setAttribute("aria-expanded", "true");
+ });
});
Since few browsers support it at this time, it's debatable whether it's ready for production use, but I think it's a very exciting technology for the future.
Conclusion
This article was about considering the implementation of an accessible accordion.
Even though it looks like a simple UI at first glance, it can be complicated when you actually try to implement it, so I realized the importance of researching it thoroughly.
Also, since there are some parts that I skipped implementing in this article (such as optional keyboard interactions), if you are interested, please take a look at the ARIA APG.
References
Discussion