You can find the <details>
element all over the web these days. We were excited about it when it first dropped and toyed with using it as a menu back in 2019 (but probably don’t) among many other experiments. John Rhea made an entire game that combines <details>
with the Popover API!
Now that we’re 5+ years into <details>
, we know more about it than ever before. I thought I’d round that information up so it’s in one place I can reference in the future without having to search the site — and other sites — to find it.
The basic markup
It’s a single element:
<details>
Open and close the element to toggle this content.
</details>
That “details” label is a default. We can insert a <summary>
element to come up with something custom:
<details>
<summary>Toggle content</summary>
Open and close the element to toggle this content.
</details>
From here, the world is sorta our oyster because we can stuff any HTML we want inside the element:
<details>
<summary>Toggle content</summary>
<p>Open and close the element to toggle this content.</p>
<img src="https://css-tricks.com/using-styling-the-details-element/path/to/image.svg" alt="">
</details>
The content is (sorta) searchable
The trouble with tucking content inside an element like this is that it’s hidden by default. Early on, this was considered an inaccessible practice because the content was undetected by in-page searching (like using CMD
+F
on the page), but that’s since changed, at least in Chrome, which will open the <details>
element and reveal the content if it discovers a matched term.
That’s unfortunately not the case in Firefox and Safari, both of which skip the content stuffed inside a closed <details>
element when doing in-page searches at the time I’m writing this. But it’s even more nuanced than that because Firefox (testing 134.0.1) matches searches when the <details>
element is open, while Safari (testing 18.1) skips it altogether. That could very well change by the end of this year since searchability is one of the items being tackled in Interop 2025.
So, as for now, it’s a good idea to keep important content out of a <details>
element when possible. For example, <details>
is often used as a pattern for Frequently Asked Questions, where each “question” is an expandable “answer” that reveals additional information. That might not be the best idea if that content should be searchable on the page, at least for now.
Open one at a time
All we have to do is give each <details>
a matching name
attribute:
<details name="notes">
<summary>Open Note</summary>
<p> ... </p>
</details>
<details name="notes"> <!-- etc. --> </details>
<details name="notes"> <!-- etc. --> </details>
<details name="notes"> <!-- etc. --> </details>
This allows the elements to behave a lot more like true accordions, where one panel collapses when another expands.
Style the marker
The marker is that little triangle that indicates whether the <details>
element is open or closed. We can use the ::marker
pseudo-element to style it, though it does come with constraints, namely that all we can do is change the color and font size, at least in Chrome and Firefox which both fully support ::marker
. Safari partially supports it in the sense that it works for ordered and unordered list items (e.g., li::marker
), but not for <details>
(e.g., summary::marker
).
Let’s look at an example that styles the markers for both <details>
and an unordered list. At the time I’m writing this, Chrome and Firefox support styling the ::marker
in both places, but Safari only works with the unordered list.
Notice how the ::marker
selector in that last example selects both the <details>
element and the unordered list element. We need to scope the selector to the <details>
element if we want to target just that marker, right?
/* This doesn't work! */
details::marker {
/* styles */
}
Nope! Instead, we need to scope it to the <summary>
element. That’s what the marker is actually attached to.
/* This does work */
summary::marker {
/* styles */
}
You might think that we can style the marker even if we were to leave the summary out of the markup. After all, HTML automatically inserts one for us by default. But that’s not the case. The <summary>
element has to be present in the markup for it to match styles. You’ll see in the following demo that I’m using a generic ::marker
selector that should match both <details>
elements, but only the second one matches because it contains a <summary>
in the HTML. Again, only Chrome and Firefox support for the time being:
You might also think that we can swap out the triangle for something else since that’s something we can absolutely do with list items by way of the list-style-type
property:
/* Does not work! */
summary::marker {
list-style-type: square;
}
…but alas, that’s not the case. An article over at web.dev says that it does work, but I’ve been unsuccessful at getting a proper example to work in any browser.
That isn’t to say it shouldn’t work that way, but the specification isn’t explicit about it, so I have no expectations one way or another. Perhaps we’ll see an edit in a future specification that gets specific with <details>
and to what extent CSS can modify the marker. Or maybe we won’t. It would be nice to have some way to chuck the triangle in favor of something else.
And what about removing the marker altogether? All we need to do is set the content
property on it with an empty string value and voilà!
Once the marker is gone, you could decide to craft your own custom marker with CSS by hooking into the <summary>
element’s ::before
pseudo-element.
Just take note that Safari displays both the default marker and the custom one since it does not support the ::marker
pseudo-element at the time I’m writing this. You’re probably as tired reading that as I am typing it. 🤓
Style the content
Let’s say all you need to do is slap a background color on the content inside the <details>
element. You could select the entire thing and set a background on it:
details {
background: oklch(95% 0.1812 38.35);
}
That’s cool, but it would be better if it only set the background color when the element is in an open
state. We can use an attribute selector for that:
details[open] {
background: oklch(95% 0.1812 38.35);
}
OK, but what about the <summary>
element? What if you don’t want that included in the background? Well, you could wrap the content in a <div>
and select that instead:
details[open] div {
background: oklch(95% 0.1812 38.35);
}
What’s even better is using the ::details-content
pseudo-element as a selector. This way, we can select everything inside the <details>
element without reaching for more markup:
::details-content {
background: oklch(95% 0.1812 38.35);
}
There’s no need to include details
in the selector since ::details-content
is only ever selectable in the context of a <details>
element. So, it’s like we’re implicitly writing details::details-content
.
The ::details-content
pseudo is still gaining browser support when I’m writing this, so it’s worth keeping an eye on it and using it cautiously in the meantime.
Animate the opening and closing
Click a default <details>
element and it immediately snaps open and closed. I’m not opposed to that, but there are times when it might look (and feel) nice to transition like a smooth operator between the open and closed states. It used to take some clever hackery to pull this off, as Louis Hoebregts demonstrated using the Web Animations API several years back. Robin Rendle shared another way that uses a CSS animation:
details[open] p {
animation: animateDown 0.2s linear forwards;
}
@keyframes animateDown {
0% {
opacity: 0;
transform: translatey(-15px);
}
100% {
opacity: 1;
transform: translatey(0);
}
}
He sprinkled in a little JavaScript to make his final example fully interactive, but you get the idea:
Notice what’s happening in there. Robin selects the paragraph element inside the <details>
element when it is in an open
state then triggers the animation. And that animation uses clever positioning to make it happen. That’s because there’s no way to know exactly how tall the paragraph — or the parent <details>
element — is when expanded. We have to use explicit sizing, padding, and positioning to pull it all together.
But guess what? Since then, we got a big gift from CSS that allows us to animate an element from zero height to its auto (i.e., intrinsic) height, even if we don’t know the exact value of that auto height in advance. We start with zero height and clip the overflow so nothing hangs out. And since we have the ::details-content
pseudo, we can directly select that rather than introducing more markup to the HTML.
::details-content {
transition: height 0.5s ease, content-visibility 0.5s ease allow-discrete;
height: 0;
overflow: clip;
}
Now we can opt into auto-height transitions using the interpolate-size
property which was created just to enable transitions to keyword values, such as auto
. We set it on the :root
element so that it’s available everywhere, though you could scope it directly to a more specific instance if you’d like.
:root {
interpolate-size: allow-keywords;
}
Next up, we select the <details>
element in its open
state and set the ::details-content
height to auto
:
[open]::details-content {
height: auto;
}
We can make it so that this only applies if the browser supports auto-height transitions:
@supports (interpolate-size: allow-keywords) {
:root {
interpolate-size: allow-keywords;
}
[open]::details-content {
height: auto;
}
}
And finally, we set the transition on the ::details-content
pseudo to activate it:
::details-content {
transition: height 0.5s ease;
height: 0;
overflow: clip;
}
/* Browser supports interpolate-size */
@supports (interpolate-size: allow-keywords) {
:root {
interpolate-size: allow-keywords;
}
[open]::details-content {
height: auto;
}
}
But wait! Notice how the animation works when opening <details>
, but things snap back when closing it. Bramus notes that we need to include the content-visibility
property in the transition because (1) it is implicitly set on the element and (2) it maps to a hidden state when the <details>
element is closed. That’s what causes the content to snap to hidden when closing the <details>
. So, let’s add content-visibility
to our list of transitions:
::details-content {
transition: height 0.5s ease, content-visibility 0.5s ease allow-discrete;
height: 0;
overflow: clip;
}
/* Browser supports interpolate-size */
@supports (interpolate-size: allow-keywords) {
:root {
interpolate-size: allow-keywords;
}
[open]::details-content {
height: auto;
}
}
That’s much better:
Note the allow-discrete
keyword which we need to set since content-visibility
is a property that only supports discrete animations and transitions.
Interesting tricks
Chris has a demo that uses <details>
as a system for floating footnotes in content. I forked it and added the name
attribute to each footnote so that they close when another one is opened.
I mentioned John Rhea’s “Pop(over) The Balloons” game at the top of these notes:
Bramus with a slick-looking horizontal accordion forked from another example. Note how the <details>
element is used as a flex container:
Chris with another clever trick that uses <details>
to play and pause animated GIF image files. It’s doesn’t actually “pause” but the effect makes it seem like it does.
Ryan Trimble with styling <details>
as a dropdown menu and then using anchor positioning to set where the content opens.