Progressively Enhanced HTML Accordion
Does what it says on the tin. Uses <details>
and <summary>
with a bit of ARIA to create an accordion that works without JavaScript while working better with JavaScript. Mostly.
See the Pen Progressively Enhanced HTML Accordion by Adrian Roselli (@aardrian) on CodePen.
Visit the standalone version for testing or in case the embed is busted.
Considerations
Now to talk about what I did, so you can comment on what I should have done.
Not a True Accordion
Four years ago (2019) I pointed out that <details>
and <summary>
and are not an accordion. I stand by that, though now that they are functional in all evergreen browsers they can form the basis of an accordion.
I do this in two key ways:
- I give them a grouping context;
- I ensure only one can be open at a time.
Grouping Context
I wrap a set of <details>
elements in a <section>
because it is the element that most closely matches what I want. Then I use ARIA to give it an accessible name by pointing aria-labelledby
to the heading that is the best fit (the only one in the page right now).
Giving it an accessible name makes <section>
into a region. This is relevant for screen reader users.
If you find the named region adds too much noise to the page for screen reader users, then give it a group
role, as I have done:
<section role="group" aria-labelledby="AccID">
Ensure Only One Can Be Open
I feel strongly that an accordion means only one in the set can be open at a time. Otherwise this is just a group of adjacent <details>
elements that probably does not need to be in a grouping context.
This is where the JavaScript comes in. I have a function that walks through all the <details>
on the page, collapses them, and then opens the one the user chose:
function closeAllDisclosures() {
var openDs = document.querySelectorAll("details[open]");
for (var i = 0; i < openDs.length; i++) {
openDs[i].removeAttribute("open");
}
}
function toggleDisclosure(btnID,prt) {
// If more than one is open, probably from an
// in-page search (Chromium browsers)
if (document.querySelectorAll("details[open]").length > 1) {
// nada
} else {
closeAllDisclosures();
}
// Get the <summary> that triggered this
var theTrigger = document.getElementById(btnID).parentNode;
// If the <details> is open...
if (theTrigger.open) {
theTrigger.closed;
// Otherwise <details> is not open...
} else {
theTrigger.open;
}
}
You can almost definitely write better JavaScript, and you should.
Also, ensure one is open on page load. Otherwise this would just be weird.
Find in Page
Chromium browsers will expand any <details>
if it contains text matching an in-browser search (Ctrl/⌘ + F). Not Firefox, not Safari.
This block of code (which you may have noticed in the function above) is an effort to account for that by allowing a <summary>
to close its own panel (something this pattern does not otherwise allow):
// If more than one is open, probably from an
// in-page search (Chromium browsers)
if (document.querySelectorAll("details[open]").length > 1) {
// nada
} else {
closeAllDisclosures();
}
If Firefox and Safari ever add this feature, I genuinely hope it works as Chromium’s does or rewriting will be needed. Assuming you would use this hot mess of spaghetti script anyway.
I do not force results in one panel to close other panels. I just let the page get longer. You do you, of course.
Layout
The overall accordion is set to fit in the viewport. As you expand or collapse panels, the height does not change but the panels instead overflow if there is too much content to fit vertically.
I also use a background shadow effect as a visual cue that this thing scrolls, originally from Lea Verou.
details > div {
height: calc(70vh - 7.5em);
overflow: auto;
padding: 0.25em 1.25em;
border: 0.1em solid #ddd;
background-color: #fff;
margin-top: -0.1em;
background:
linear-gradient(var(--page-bg) 30%, var(--page-bg)),
linear-gradient(var(--page-bg), var(--page-bg) 70%) 0 100%,
radial-gradient(farthest-side at 50% 0, rgba(0,0,0,.2), rgba(0,0,0,0)),
radial-gradient(farthest-side at 50% 100%, rgba(0,0,0,.2), rgba(0,0,0,0)) 0 100%;
background-repeat: no-repeat;
background-color: var(--page-bg);
background-size: 100% 4em, 100% 4em, 100% 1.4em, 100% 1.4em;
background-attachment: local, local, scroll, scroll;
}
If the JavaScript does not run, then this really only ensures that each individual opened panel is no taller than the viewport. So it still has some purpose.
Keyboard Users
<details>
and <summary>
work just fine with a keyboard. That is not what this section addresses.
While Firefox will make those scrolling areas keyboard focusable by default, Chromium and Safari do not (though rumor has it Chrome is finally rolling out that fix in a couple versions).
If your disclosed content has interactive bits within it, then you need not do anything more. If it does not, then you may have to do the painful but still necessary HTML and ARIA I outline in my post Keyboard-Only Scrolling Areas, which I employ a lot (such as my Under-Engineered Responsive Tables).
Those scrolling containers might then end up looking like this (referencing the text in the <summary>
for its accessible name):
<div tabindex="0" role="region" aria-labelledby="Acc01">
My example does not have that because each panel has interactive controls (links), so a keyboard user can get into them and scroll regardless of browser.
Media Types
I am lumping media queries and feature queries together here.
Forced Colors
Windows High Contrast Mode (WHCM), Windows 11 Contrast Themes, CSS forced colors, whatever you call it, they all mean the same thing. I wrote a primer on WHCM, system colors, and the feature query, in fact.
Anyway, there is nothing you need to do here. Because this uses native HTML elements it just kinda works.
This is a two-parter, essentially. Use CSS to clear the borders, remove all the spacing, hide the SVG arrow thinger, and remove the arbitrary height. Now any open panel will take up as much space on the printed page as its content dictates.
@media print {
summary, section[role="group"], details > div {
border: none;
background: transparent;
padding: 0;
}
summary span {
display: none;
}
details > div {
height: auto;
}
}
The JavaScript takes this a step further by opening all the panels for print. After printing, it closes all but the one you had open.
function openAllDisclosures() {
var allDs = document.querySelectorAll("details");
for (var i = 0; i < allDs.length; i++) {
allDs[i].setAttribute("open","");
}
}
// Handling expanding all, restoring for printing
var openD;
window.addEventListener("beforeprint", (event) => {
openD = document.querySelector("details[open] summary").id;
openAllDisclosures();
});
window.addEventListener("afterprint", (event) => {
closeAllDisclosures();
document.getElementById(openD).parentNode.setAttribute("open","");
});
The <summary>
element acts as the visual heading on the printed page. If you are exporting it as a tagged PDF, then you will need to go into the PDF output and convert these to headings.
Support
In my post Disclosure Widgets, I compared screen reader support for native and ARIA disclosure widgets. It is mostly the same, with one change.
Announcements for common pairings:
- JAWS 2023 / Chrome 115
- accName button [collapsed|expanded]. To activate press Enter.
- NVDA 2023.1 / Firefox 116
- accName [collapsed|expanded].
- Narrator Windows 11 22H2 / Edge 115
- accName disclosure triangle [collapsed|expanded].
- VoiceOver macOS 13.4.1 / Safari 16.5.1
- accName [collapsed|expanded] summary, group.
- Orca / Ubuntu 22.04.2 LTS / Firefox 115
- accName [collapsed|expanded] pushbutton.
- Android 13 / Chrome 115 / TalkBack 13.1
- [collapsed|expanded] accName disclosure triangle.
- Android 13 / Firefox 116 / TalkBack 13.1
- accName.
- VoiceOver iPadOS 16.5 / Safari 16.5
- accName.
There is a regression in Safari on iPadOS. It no longer announces the control state nor instructions to operate, making these essentially meaningless to iPad VoiceOver users. But I can still activate them with voice control.
Voice control works across the platforms that have it.
The triggers are all keyboard and pointer operable.
Updated 28 January 2024: The <summary>
now works in iPadOS / Safari 17.3.
- VoiceOver iPadOS 17.3
- accName [collapsed|expanded]. Double-tap to [collapse|expand].
Wrap-up
I am of the opinion that accordions mostly slow users down. They require extra interaction just to get to additional content, often because some product owner thought the page was “too cluttered” after they jammed all their words into it (or assume users are incapable of scanning). Essentially accordions are lazy carousels for organizations that lack brevity.
That being said, unless you have iPad VoiceOver users, using native HTML with a little JavaScript will do the trick. Otherwise you are stuck with ARIA disclosures that do not do progressive enhancement the way you may want (but leaving all of them open is what I may want as a user).
If nothing else, this post should have shown you stuff to consider as you build your own thing and maybe reminded you that Safari has earned its “New IE” moniker for good reason.
Almost certainly you have your own opinions about this pattern, my code, and maybe even the lack of design effort. Use that comment form. Remember to escape <
and >
(as <
and >
).
Also, maybe go read Scott’s The details and summary elements, again for more info on support and weirdness.
Update: 10 August 2023
I built this not to justify or promote the overall accordion pattern, but to demonstrate a method if you have to build it.
As such, I received plenty of questions on the socials, Slacks, emails, etc. asking why this does not allow a user to close the currently open pattern. My answer is simple — then it no longer meets how I have defined an accordion in this post.
And yes, I explored disabling the <summary>
, which screen reader and keyboard users found confounding (the former owing to removing context, the latter for managing focus).
As for the broader pattern, OpenUI has termed my model as an “exclusive accordion”, where only one item can be open at a time (or “exact exclusive”, where that one item cannot be closed). WHATWG HTML has an open issue to use the name
attribute to make this pattern happen natively in HTML. Chrome is adding support.
I also probably should have linked my post Avoid aria-roledescription, since I explain why I am not using it here.
Quite simply, it is my opinion that if you have an accordion where all panels can be closed or more than one can be open then it is no longer an accordion. It is just a bunch of adjacent disclosure widgets. If you want one of those, then this post is not for you, though some of the nuggets of what I do in here should still be useful.
Also: don’t use this for menuing; Steven Hoober gathers some risks of accordions; my responsive accordion is yet more user-hostile.
Update: 24 August 2023
If you are one of the people who read this post and were more hung up on the fact that only one item can be open at a time (and one must always be open), then you may be interested in this Open UI (W3C Community Group) poll from L. David Baron’s W3C Mastodon account:
Do you write HTML? Might you have an opinion about how an extension to the <details> element (for grouping multiple <details> elements into an exclusive accordion) should behave? Have a look at the poll at github.com/openui/open-ui/issues/812 and vote for one of the options!
The poll is essentially asking what happens when an author (or the outcome of a scripted action) specifies that two panels should be open in the accordion. You can vote with one of the pre-defined emoji responses or, if you are the wordy type who thinks your use case is unique, leave a comment. I am in the latter category.
Update: 1 September 2023
I took the sample accordion and reformatted it as a brief overview of colors I have seen used in messaging systems (to denote errors or warnings). Then I rotated it 52° and set it in a 16:9 rectangle. It is not meant for real use. The white on red fails WCAG text contrast requirements. It is not internationalization-friendly. It only rotates for mobile. As an “exclusive” accordion, once you open one you can never have them all closed again (so refresh the page).
Yes, that is the 2021 redesign of the disability pride flag.
See the Pen Common Application Message Colors by Adrian Roselli (@aardrian) on CodePen.
Editable pen and debug mode available.
Update: 17 November 2023
As I noted above, I consider accordions to generally be user hostile. This post is an attempt to at least make them equally user hostile for all users. I also noted above that Open UI pushed a WHATWG HTML spec change to create a native accordion, and since those updates I have weighed in on conversations at Open UI and at HTML AAM.
Eric Eggert has penned Exclusive accordions exclude, where he shares his frustrations with accordions and the process that got them into WHATWG HTML.
Remember, I still maintain an accordion is a pattern where only one disclosure can be open at a time. Otherwise it is simply a pile of possibly related disclosures. Eric (and most folks in the conversations I linked) refer to those as “exclusive accordions”.
Update: 28 November 2023
Today Chromium issue 1444057: implement support for exclusive accordions in <details> element was marked as “fixed”, with an earlier note that it would ship in Chrome 120. I grabbed Chrome Canary (which is version 121), forked my pen above, and confirmed support.
The forked pen for your own testing (embedded below) or you can grab the forked debug view for better testing in Chrome Canary.
See the Pen HTML Accordion Using Only `name` by Adrian Roselli (@aardrian) on CodePen.
So far in Chrome a disclosure gives no context that there are others (via the accessibility tree). David Baron pointed me to Chrome’s work in progress to convey the quantity of disclosures and one’s current position within them.
In the meantime, if you search for the term “progress” in the example you will see how one disclosure closes as a hit is found in another disclosure. We can see the outcome of our collective (plurality of) survey responses in action.
7 Comments
Even if accordions are technically supposed to only have one section open at a time, please always provide Expand All and Collapse All for those of us for who it is problematic to engage with accordions and would rather scroll.
In response to .At that point it is no longer an accordion but a bunch of adjacent and independent disclosure widgets. In which case I agree and this post does not apply.
> and work just fine with a keyboard
Apart from when there is a focussable element inside the hidden in safariif you tab through it tabs to the element inside the ‘accordion’
In response to .I confirmed your experience with Safari 16.5 and Safari TP 176 on macOS 13.4.1. At least, I confirmed it does it with a text input, but not with a link.
I could not find an open WebKit bug for this. Do you know of one?
In response to .Gavin, I could not find an open WebKit issue, so I filed one: Bug 260523 – Form fields hidden in collapsed details/summary get focus.
In the meantime, the approach outlined in this post still works. Even if Safari does not handle nested form elements properly.
As of Chrome 120 (released late 2023), to create an exclusive accordion, add a name attribute to the elements. When this attribute is used, multiple elements that have the same name value form a semantic group and they will behave as an exclusive accordion. https://developer.chrome.com/docs/css-ui/exclusive-accordion#the_exclusive_accordion
In response to .Aaron, indeed it has been out for a while now (and my late 2023 update above confirming Chrome 120 support provides an example for testing). I still have issues with the implementation (which a few of us discussed at TPAC and for which efforts are ongoing): the lack of a group name / context and the failure to provide a count of related accordions.
Leave a Comment or Response