← All writing
Craft · · 6 min

details and summary

What you get without writing anything, and why your FAQ prints as a list of questions with no answers.

HTML Accessibility

faq.html
<details>
  <summary>How long does delivery take?</summary>
  <p>Two to three working days, or next day if you order before noon.</p>
</details>
HTML

That’s a disclosure widget. It opens and closes on click, it’s operable with the keyboard, it announces its expanded state to assistive technology, it’s in the tab order, and there is no JavaScript anywhere.

The version I was sent had about 200 lines: click handlers, an aria-expanded toggle, a height animation, a keydown handler for Enter and Space, and a bug where the arrow key didn’t move between items.

What you get without writing anything

The state toggling, obviously.

Keyboard operation, correctly: Enter and Space both work because <summary> is natively interactive: the same argument as the button one from the delegation post.

The right announcement. A screen reader says the summary text and whether it’s expanded or collapsed, with no ARIA from you. Every hand-rolled version I’ve seen either forgets aria-expanded or forgets to update it, and the second is worse because it’s confidently wrong.

A toggle event, if you need to know.

And, in some browsers, find-in-page will open a closed section when the match is inside it, which no custom accordion has ever done for me.

The name attribute, which is new

The reason this post is worth writing now rather than in 2020.

The one thing <details> couldn’t do was behave like an accordion, where opening one section closes the others. That needed JavaScript, and it’s the single most common reason people reached for a library.

accordion.html
<details name="faq">
  <summary>How long does delivery take?</summary></details>

<details name="faq">
  <summary>Can I change my order?</summary></details>
HTML

Same name, exclusive group. Opening one closes the rest. It’s the same mechanism as radio buttons and it reads exactly right once you see it.

Two engines shipped it last winter and the third is expected shortly. And the degradation is the nicest kind: a browser without it just lets you open several at once, which nobody will report as a bug.

Same name, exclusive group. It's the radio button mechanism, and it retires the last real reason anybody wrote an accordion in JavaScript.

I’d add one caution, which is that exclusive accordions are often the wrong design. If somebody wants to compare two answers, closing the first one when they open the second is actively hostile. I’d default to independent sections and use the grouping only where the content is genuinely alternatives.

Styling it

This used to require a vendor-prefixed pseudo-element and a lot of swearing. Now:

details.css
summary {
  cursor: pointer;
  font-weight: 600;
}

summary::marker { color: var(--accent); }     /* the triangle */

/* or replace it entirely */
summary { list-style: none; }
summary::after { content: "+"; float: right; }
details[open] summary::after { content: "−"; }
CSS

summary is display: list-item by default. That’s why ::marker works and why list-style: none removes the triangle. details[open] gives you the open state for anything else.

The print problem

The one I’d most like people to know, and it’s a callback to a post I wrote almost a decade ago about print stylesheets.

Content inside a closed <details> is not rendered. Which means it isn’t printed.

So an FAQ page built as an accordion prints as a list of questions with no answers. A “terms and conditions” page with collapsible sections prints as a table of contents. A product page where the specification is in a disclosure prints without the specification.

And these are exactly the pages people print or save as PDF: terms, specifications, policies, instructions.

print.css
@media print {
  details { display: block; }
  details > *:not(summary) { display: block !important; }
  summary { list-style: none; }
}
CSS

There’s an open attribute you can also force on with a line of script before printing, more reliable across browsers, and either way it’s about four lines and nobody does it. I hadn’t, on two sites, until I went and checked while writing this.

Where it’s the wrong element

Anything that overlays. A dropdown menu that sits on top of the page wants the popover attribute now: top layer, light dismiss, no z-index fight. <details> pushes the page around instead: right for a section of content, wrong for a menu.

Anything interactive inside the <summary>. A link or a button in there fights with the summary’s own activation and the results differ by browser. Keep the summary to text.

Anything that must be visible. Important information hidden behind a click is still hidden, and “we put it in an accordion” is how a lot of sites make their returns policy technically present and practically invisible.

The one thing it can’t do

Animate. The height goes from nothing to whatever the content is, and auto isn’t a value CSS has historically been able to transition to, so there’s no clean native slide-open.

Which is the reason a lot of the JavaScript accordions exist, and I’d rather say that than pretend the reason was ignorance. Somebody wanted it to slide and the element wouldn’t.

There’s work in progress to make transitions to intrinsic sizes possible, which would close this off entirely, and it isn’t broadly available yet. Until then the honest options are: don’t animate, animate only the content’s opacity and transform, or accept the JavaScript for this one property.

200 lines to two elements

Plus four lines of print CSS that the 200 lines never had either.

The theme, again: it isn’t that the custom version is longer. It’s that the custom version quietly loses the keyboard support, the announcement, the find-in-page, and the printing, and none of those losses generate a bug report.

Read similar posts
6 min

Heading levels are an outline

On a page I audited last month the only `h1` was the logo, the section headings were all `h4` because `h4` was the right size, and the actual page title was a `div` with a class on it.

6 min

dialog

I have built the same modal four times across four projects, each one slightly different, each one getting a different two of the six behaviors wrong, and there has been an element for it since before the second one.