Printing an accordion
2 elements replaced about 200 lines of JavaScript and fixed everything except the bug that started the ticket.
A customer saved one of our client’s returns pages as a PDF, emailed it back with a question about the third item, and what came through was a column of questions with white space under all of them.
It reached us as “the PDF export is broken,” which was generous, because there is no PDF export. There’s a browser, a print stylesheet somebody wrote in 2019, and a customer with ctrl+p.
The page was an FAQ accordion. About 200 lines of JavaScript, click handlers, an aria-expanded toggle, a height animation, a keydown handler for Enter and Space, and a bug where the arrow keys didn’t move between items, which nobody had reported because nobody arrows through an FAQ.
Nothing was broken in the browser. A closed panel was display: none, and display: none doesn’t mean “hidden until you print,” it means the browser never lays the thing out at all, so the print stylesheet had nothing to work on. The answers weren’t collapsed in that PDF. They were never drawn.
There were two jobs in that ticket and I did them in the wrong order. I rebuilt the component natively because I’d been wanting an excuse, and then I fixed the reported bug with 3 lines of print CSS that would have worked exactly as well on the 200 lines I’d just deleted.
<details>
<summary>How long does delivery take?</summary>
<p>Two to three business days, or next day if you order before noon.</p>
</details>
That’s the whole component. It opens and closes on click, it works from the keyboard, it tells assistive technology what state it’s in, and there is no JavaScript anywhere in it.
Some assembly not required
The toggling is the obvious part and the least interesting one. You actually stop writing everything around it.
Enter and Space both open it, because <summary> is natively interactive the way a <button> is. Focus, tab order and both activation keys arrive as a set, or you reimplement all three by hand and forget one of them.
Screenreaders announce the summary text along with whether it’s expanded or collapsed. You write no ARIA to get that. Every hand-rolled accordion I’ve opened either forgets aria-expanded or forgets to update it after the click, and the second one is worse, because a confidently wrong state is harder to recover from than a missing one.
There’s a toggle event if you need to know when it opens, which is handy for loading something heavy inside on demand. And in some browsers find-in-page will open a closed section when the match is inside it, which is a thing no custom accordion has ever done for me.
One at a time, please
This is worth writing in 2024 instead of 2020 because <details> finally does the accordion part.
The one behavior it couldn’t do was act as a group, where opening one section closes the others, and my sense is that’s the single most common reason people reached for a library. Now the sections take a name.
<details name="faq">
<summary>How long does delivery take?</summary>
…
</details>
<details name="faq">
<summary>Can I change my order?</summary>
…
</details>
Same name, one open at a time. It’s radio buttons, and once you’ve seen that it’s radio buttons you can’t unsee it. Two engines released it last winter and the third is expected soon, and the degradation is the benign kind: a browser without it lets you open several sections at once, which nobody on earth is going to file as a bug.
Though I’d push back on the pattern more often than I’d reach for it. If somebody is comparing two answers, slamming the first one shut when they open the second is hostile, so I default to independent sections and save the grouping for content that’s genuinely a set of alternatives.
And if the panel is meant to sit on top of the page instead of pushing it around, this is the wrong element entirely and you want the popover attribute I wrote about in the spring, or a modal if it’s something somebody has to deal with before they can move on.
Now with 100% fewer vendor prefixes
Styling one of these used to mean a prefixed pseudo-element and a lot of swearing. It doesn’t anymore.
summary {
cursor: pointer;
font-weight: 600;
}
/* the triangle */
summary::marker {
color: var(--accent);
}
/* or take it off and draw your own */
summary {
list-style: none;
}
summary::after {
content: "+";
float: right;
}
details[open] summary::after {
content: "−";
}
summary is display: list-item by default, which is why ::marker works on it and why list-style: none takes the triangle away. details[open] gets you the open state for anything else the design wants.
No slide for you
You can’t animate it. The content goes from no height to whatever height it turns out to be, and auto has never been a value CSS could transition to. So there’s no clean native slide open.
That’s the real reason a lot of those JavaScript accordions exist. I’d rather say so than pretend everybody who wrote one was a bozo who didn’t read the docs. Somebody wanted it to slide and the element wouldn’t do it.
There’s work happening on transitioning to intrinsic sizes, which would close the gap off for good. It isn’t broadly available yet. Until then the choices are (1) don’t animate, (2) animate the content’s opacity and transform and leave the height alone, or (3) keep the JavaScript for that one property and nothing else.
Go and print it yourself
Back to the returns page, for which the native rebuild did precisely nothing. Content inside a closed <details> isn’t rendered either, for the same reason as before. So I had swapped one accordion that prints as headings for another accordion that prints as headings, with better keyboard support.
An FAQ prints as a list of questions. A terms page with collapsible sections prints as a table of contents. A product page with the specification in a disclosure prints without the specification. And those are exactly the pages people print or save: terms, specs, policies, instructions, returns.
@media print {
details {
display: block;
}
details > *:not(summary) {
display: block !important;
}
summary {
list-style: none;
}
}
That works in most places. If you want it to work everywhere, set the open attribute on every <details> from a line of script on beforeprint, which is the more reliable version and still barely qualifies as work.
I wrote about print stylesheets years ago, after a client’s warehouse turned out to be printing every single order. I’ve been quietly smug about it ever since. Then I went and printed a few pages of my own while writing this, and two of my sites have accordions with nothing underneath the headings. So, uh. Rocks and glass houses.
The swap bought me all the invisible stuff, the keyboard and the announcement and the state, none of which anybody was ever going to report, because a bug report requires somebody to notice. The one thing an actual customer did notice was mine in both versions, and the fix for it was three lines in a stylesheet I’d already written and never finished. That ratio has stuck with me. I now print anything that hides content behind a click before I call it done.