The devil is in the details: a look into a disclosure widget markup
by Cristian Diaz published on
Disclosure widgets are one of the most common component patterns you can find on the web. It consists of a button that can hide or show information when you click it. It's also one of the straightforward components to make from a technical standpoint.
Just a quick note: this article will focus on the most basic form of it to show or hide content. A navigation menu can be considered as a disclosure widget but it has other features that won’t be the main focus of this article.
This component pattern has two approaches: the first one is creating a <button>
element with the attribute aria-expanded that will toggle between true
and false
when you want the content to be shown or hidden. Additionally, you want to create a relationship between those two elements with aria-controls to indicate the button controls the presence of this element, like this:
<button aria-expanded="false" aria-controls="accordion">What are cephalopods?</button>
<div id="accordion">
<p>A cephalopod is any member of the molluscan class <i>Cephalopoda</i> such as a squid, octopus, cuttlefish, or nautilus.</p>
</div>
This approach requires JavaScript to modify the aria-expanded
attribute. However, we can pair it with a pure CSS’ next-sibling combinator to control showing and hiding the disclosure’s content.
button[aria-expanded="false"] + div {
display: none
}
const button = document.querySelector("button")
button.addEventListener("click", (element) => {
const isExpanded = element.target.getAttribute("aria-expanded")
if (isExpanded === "true") {
element.target.setAttribute("aria-expanded", "false")
} else {
element.target.setAttribute("aria-expanded", "true")
}
})
Creating a disclosure widget that requires JavaScript also means you should work on a progressively enhanced version of it in case JavaScript doesn't load. There are multiple approaches to this, a very common one is using heading instead of buttons, and when JavaScript loads, replacing the header with a button:
<h3>What are cephalopods</h3>
<div>
<p>A cephalopod is any member of the molluscan class <i>Cephalopoda</i> such as a squid, octopus, cuttlefish, or nautilus.</p>
</div>
All of this sounds like quite some work for a relatively simple component pattern, it'd be cool if we had a native option to make a disclosure widget... Oh, wait, we have one! This is where our second approach enters, and that's using the <details>
and <summary>
elements. Let's come back to our initial markup and let's modify it with this approach.
<details>
<summary>What are cephalopods?</summary>
<p>A cephalopod is any member of the molluscan class <i>Cephalopoda</i> such as a squid, octopus, cuttlefish, or nautilus.</p>
</details>
And that's it! This disclosure widget works, and it's accessible, so I guess the answer would be using this one instead of using the first approach, right? Well, as usual, the reality is more nuanced than that. As much as I'd prefer to use native options for component patterns, the answer to that question is a definitive “It depends”.
When picking a disclosure widget markup, the author needs to balance the strengths and limitations of each approach, and consider what content it will contain. This article will talk about those nuances you should keep in mind about choosing one markup approach or another, mostly focusing on screen reader accessibility because both of them meet the requirements of keyboard navigation with no problem at all.
Experience across different browsers
The first factor to keep in mind is consistency. When you use a progressively enhanced button approach, you know that it'll be the same for screen readers. Both markup approaches will read them as a button and will read the aria-expanded
attribute similarly. But with <details>
and <summary>
things change quite a bit when you start taking into consideration different screen readers and browsers.
When you use <details>
and <summary>
, NVDA and JAWS both narrate it as a button in Chrome, and they'll mention if the element is collapsed or expanded. But when you use the same markup in Firefox, they narrate the marker (that is, the visual indicator is being used to indicate the element is collapsed or expanded) as well. My previous example will be read as “Filled right pointing small triangle, what are cephalopods, button, collapsed”. And when we start talking about mobile, the experiences vary a lot.
This is not bad per se, but in some browsers, mentioning the marker can feel a bit like noise, but buttons on the other hand offer a cleaner more consistent experience.
The key point in this section is that you should ask yourself this question: am I okay with letting the screen reader experience be different between different screen readers and browsers? If you're all right with this, use <details>
and <summary>
, but if you'd prefer a more consistent experience all around, use a progressively enhanced <button>
.
Wait, I can modify this default marker, right?
By default <details>
and <summary
have this triangle arrow that will change when it is expanded or collapsed. Maybe you have a design that has a different indicator, can you modify that? The answer is yes, but there is a huge caveat here:
As Manuel mentions in his article details/summary inconsistencies, you can modify the default arrow by using those CSS rules:
summary {
list-style: none;
}
This will hide the arrow in all browsers except Safari, so you’ll need an additional CSS rule:
summary::-webkit-details-marker {
display: none;
}
This deletes the indicator in all browsers, but this brings a big problem: as Manuel's tests in the same article show, some combination of screen readers and browsers will use the arrow indicator as part of the accessible name, and if you remove them, it'll stop indicating its state once the user press the <summary>
element.
This behavior is particularly notorious with VoiceOver and Firefox because when you remove the marker, it'll indicate nothing when the user expands the content.
For this reason, my suggestion here is that if the design has a different indicator for the expanded/collapsed states than the default option, you should use a progressively enhanced <button>
approach. On the other hand, if you are ok with the default marker, you can stick with <details>
and <summary>
.
Browser's in-page search
Those previous points are making look <details>
and <summary>
as an unreliable option, but there is something that those elements add to the table that a progressively enhanced button option can't.
As Manuel mentions in his article the details element and in-page search, Chromium-based browser in-page search option can look for content even when the <summary>
element is collapsed. This is an important factor to keep in mind because this improves usability for someone who uses the in-page search.
As a side note, do you remember when I said I would not focus on disclosure widget variants like a navigation menu? Well, between the different ways a screen reader displays this component and now taking into consideration the in-page search option, makes <details>
and <summary>
unsuitable for a navigation menu.
So, the key point of this section: if you think letting the user find a keyword using the browser in-page search is important, stick with <details>
and <summary>
, otherwise, use a progressively enhanced button.
Do I need to always hide the content?
The main selling point of <details>
and <summary>
is that they'll hide the disclosure widget’s content always, even where there is no JavaScript involved. This is something really important in some scenarios.
The first example that comes into my mind is a content warning if it's visible, can cause legitimately bad experiences or trigger a PTSD (Post traumatic stress disorder) episode for a person.
You could use a progressively enhanced button, but if JavaScript doesn't load, there is a risk, and depending on the context, it can be critical to hide this content even in those scenarios. Kitty Giraudel created a content warning component using only <details>
and <summary>
in her article, A content warning component.
My key point here is that you need to consider how critical is to hide the content as an initial state, no matter what. If it's necessary, consider using <details>
and <summary>
, otherwise, using a progressive enhanced button is a good option.
Use of headings
There is quite an interesting thing you can do with a disclosure widget made with the progressively enhanced <button>
, and that's adding headings on them. This makes it easier for screen reader users to navigate to this part of the content. The process changes a bit depending on what approach you want to use. For this approach, the markup would look like this:
<h3>
<button aria-expanded="false" aria-controls="accordion">
What are cephalopods?
</button>
</h3>
<div id="accordion">
<p>A cephalopod is any member of the molluscan class <i>Cephalopoda</i> such as a squid, octopus, cuttlefish, or nautilus.</p>
</div>
There is a catch though: the <button>
is now inside the heading, so it’ll be undetected by the next-sibling combinator. There are two options: we can use to solve this issue:
Modifying our script: We can make a slight modification in our script and making the adjacent container have the hidden
attribute .Then, we toggle it when the <button>
has the proper aria-expanded
value, like this:
const button = document.querySelector("button");
const accordionElement = document.querySelector("[data-content]");
button.addEventListener("click", (element) => {
const isExpanded = element.target.getAttribute("aria-expanded");
if (isExpanded === "true") {
accordionElement.setAttribute("hidden", "");
element.target.setAttribute("aria-expanded", "false");
} else {
accordionElement.removeAttribute("hidden");
element.target.setAttribute("aria-expanded", "true");
}
});
:has()
: The other option is using the (relatively) new CSS :has()
selector like this:
h3:has([aria-expanded="false"]) + div {
display: none;
}
The problem with :has()
is its browser support. At the moment of writing this article, Firefox just shipped it in the latest version (121), so browser support may make use of this selector not robust enough to be reliable (for now!).
Can you do that with <details>
and <summary>
? Yes, do I recommend it to do it? No, it has some inconsistent and even buggy behaviors when you use a screen reader to activate them.
Let's start with checking how it is “possible”, first, we add our heading inside <summary>
like this:
<details>
<summary><h3>What are cephalopods?</h3></summary>
<p>A cephalopod is any member of the molluscan class <i>Cephalopoda</i> such as a squid, octopus, cuttlefish, or nautilus.</p>
</details>
Keep in mind you need to add the <summary>
element as the direct child of <details>
, otherwise, it’ll not be recognized as our disclosure’s trigger. Now, There is one detail, this is how it'd look:
Then, we'd need to use CSS to put the heading at the same level, but if you do it with, for example, display: flex
, it'll not show the marker, and as we saw before, this will affect screen reader experience for some combinations of screen reader and browser. My approach here was making the heading inline, and that worked well, like this:
summary h3 {
display: inline;
}
Why I don't recommend doing that with <details>
and <summary>
? With both approaches, you can use screen reader shortcuts to navigate directly to a heading, but there is a caveat here:
With the <button>
approach there will be no problem: the button is inside the heading and if the screen reader's virtual cursor is over the heading, you can control the button by pressing the Enter
or Space
keys. This is useful when you use screen reader’s shortcuts to navigate to a heading (like using the H key).
There catch here comes with <details>
and <summary>
because it has way less consistent results. To prove that, I decided to test each disclosure with heading markup next to each other. You can take a look at the example I used right here:
See the Pen Untitled by Cristian Diaz (@ItsCrisDiaz) on CodePen.
This test used the next combinations of screen readers and browser:
- NVDA and JAWS with both Chrome and Firefox.
- Narrator with Edge
- VoiceOver with Safari, Chrome and Firefox
And those are the results:
- NVDA works perfectly with both browsers, you can navigate using the key shortcuts and you can activate them with
Enter
orSpace
. - With JAWS you can’t navigate to a heading inside the
<summary>
element, making this feature not advisable. - Narrator with Edge is surprisingly buggy, it read both headings at the same time as if they were just one, and it didn’t let me to activate any of them. However, once I removed the
<details>
element it worked normally, so it’s safe to say adding a heading inside<summary>
is not advisable. - With VoiceOver, you can navigate to a heading using the key shortcuts, however, you can’t activate the
<summary>
element that way. You can activate it navigating with theTab
key but for some reason, if VoiceOver visual cursor is on the heading itself, it can’t be activated. This can create confusion for screen reader users because they found a heading, but they will not find extra content below it because it's a disclosure widget.
So, key point of this section: if you think adding heading navigation to your disclosure widget improves the user experience for screen reader users, you'd be better using a <button>
element, otherwise, use <details>
and <summary>
.
Wrapping up
Having a native way to make a disclosure widget is great! But as usual, things are not that easy because browsers' implementation tend to vary in details that you need to take into consideration. It's not the first HTML element that has inconsistencies among browser (As an example, just look at Manuel's article O dialog focus, where art thou? to check how browsers implement keyboard focus with the <dialog>
element) and it certainly won't be the last.
In my mind, there is no doubt that browser’s implementation of <details>
and <summary>
will be more consistent in the future and this article might end up being a thing of the past. But for now, there are important considerations you need to keep in mind before deciding to use one markup approach or another.
About Cristian Diaz
Cristian is a Front-End developer from Colombia. He enjoys writing about what he learns in his blog and he decided to focus his career on helping to make web content more accessible to everybody. His main areas of expertise are HTML, CSS, and Web accessibility.
Website/blog: itscrisdiaz.com/links/
GitHub: ItsCrisDiaz
LinkedIn: itscrisdiaz
Twitter: @ItsCrisDiaz
Mastodon: @ItsCrisDiaz@mastodon.social