Written by Rob O'Leary✏️
The <details>
and <summary>
HTML elements, collectively referred to as a disclosure widget, are not easy to style. People often make their own version with a custom component because of the limitations. However, as CSS has evolved, these elements have gotten easier to customize. In this article, I will cover how you can customize the appearance and behavior of a disclosure widget.
How do <details>
and <summary>
work together?
<details>
is an HTML element that creates a disclosure widget in which additional information is hidden. A disclosure widget is typically presented as a triangular marker accompanied by some text.
When the user clicks on the widget or focuses on it and presses the space bar, it opens and reveals additional information. The triangle marker points down to indicate that it is in an open state:
The disclosure widget has a label that is always shown and is provided by the <summary>
element. This is the first child. If it is omitted, a default label is provided by the browser. Usually, it will say "details":
You can also provide multiple elements after the <summary>
element to represent the additional information:
<details>
<summary>Do you want to know more?</summary>
<h3>Additional info</h3>
<p>The average human head weighs around 10 to 11 pounds (approximately 4.5 to 5 kg).</p>
</details>
Styling <details>
and <summary>
There are a few interoperability issues that should be considered when styling the <details>
and <summary>
elements. Let's cover the basics before we get into some common use cases.
The <summary>
element is similar to a [<li>](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/li)
element because its default style includes display: list-item
. Therefore, it supports the [list-style](https://developer.mozilla.org/docs/Web/CSS/list-style)
shorthand property and its longhand properties. The browser support for the list-style
properties is quite good, but Safari is still lagging.
The disclosure widget has two pseudo-elements to style its constituent parts:
- The
::marker
pseudo-element: Represents the triangular marker that sits at the beginning of<summary>
. The styling story for this is a bit complicated. We are limited to a small set of CSS properties. Browser support is good for::marker
, but Safari doesn’t currently support the complete set of properties. I will discuss this in more detail in the “Styling the summary marker” section of this article - The
::details-content
pseudo-element: Represents the “additional information” of<details>
. This is a recent addition, so browser support is currently limited to Chrome
In the following sections, I will demonstrate some of the newer, lesser-known ways to customize a disclosure widget.
Animating the open and close actions
When you open a disclosure widget, it snaps open instantly. Blink, and you will miss it!
It is preferable to transition from one state to another in a more gradual way to show the user the impact of their action. Can we add a transition animation to the opening and closing actions of a disclosure widget? In short, yes!
To animate this, we want the height of the hidden content to transition from zero to its final height. The default value of the height
property is auto
, which leaves it to the browser to calculate the height based on the content. Animating to a value of auto
was not possible in CSS until the addition of the [interpolate-size](https://nerdy.dev/interpolate-size)
property. While browser support is a bit limited for the newer CSS features we need to use — chiefly interpolate-size
and ::details-content
— this is a great example of a progressive enhancement. It will currently work in Chrome!
Here's a CodePen example of the animation.
How does the disclosure animation work?
First, we add interpolate-size
so we can transition to a height of auto
:
details {
interpolate-size: allow-keywords;
}
Next, we want to describe the closed style. We want the “additional info” content to have a height of zero and ensure that no content is visible, i.e., we want to prevent overflow.
We use the ::details-content
pseudo-element to target the hidden content. I use the block-size
property rather than height
because it's a good habit to use logical properties. We need to include content-visibility
in the transition because the browser sets content-visibility: hidden
on the content when it is in a closed state — the closing animation will not work without including it:
/* closed state */
details::details-content {
block-size: 0;
transition: content-visibility, block-size;
transition-duration: 750ms;
transition-behavior: allow-discrete;
overflow: hidden;
}
The animation still won’t work as expected because the content-visibility
property is a discrete animated property. This means that there is no interpolation; the browser will flip between the two values so that the transitioned content is shown for the entire animation duration. We don't want this.
If we include transition-behavior: allow-discrete;
, the value flips at the very end of the animation, so we get our gradual transition.
Also, we get content overflow by setting the block-size
to 0
when the disclosure widget is in an intermediate state. We show most of the content as it opens. To prevent this from happening, we add overflow: hidden
.
Lastly, we add the style for the open state. We want the final state to have a size of auto
:
/* open state */
details[open]::details-content {
block-size: auto;
}
Those are the broad strokes. If you would prefer a more detailed video explanation, check out Kevin Powell's walkthrough for how to animate <details>
and <summary>
.
Are there any other considerations when animating a disclosure widget?
The disclosure widget may grow horizontally if the “additional information” content is wider than the <summary>
content. That may cause an unwanted layout shift. In that case, you may want to set a width on <details>
.
Like any animation, you should consider users who are sensitive to motion. You can use the prefers-reduced-motion
media query to cater to that scenario:
>@media (prefers-reduced-motion) {
/* styles to apply if a user's device settings are set to reduced motion */
details::details-content {
transition-duration: 0.8s; /* slower speed */
}
}
Implementing an exclusive <details>
group (exclusive accordion)
A common UI pattern is an accordion component. It consists of a stack of disclosure widgets that can be expanded to reveal their content. To implement this pattern, you just need multiple consecutive <details>
elements. You can style them to visually indicate that they belong together:
<details>
<summary>Payment Options</summary>
<p>...</p>
</details>
<details>
<summary>Personalise your PIN</summary>
<p>...</p>
</details>
<details>
<summary>How can I add an additional cardholder to my Platinum Mastercard</summary>
<p>...</p>
</details>
The default style is fairly simple:
Each <details>
occupies its own line. They are positioned close together (no margin or padding) and are perceived as a group because of their proximity. If you want to emphasize that they are grouped together, you could add a border and give them the same background styles as shown in the example below:
A variation of this pattern is to make the accordion exclusive so that only one of the disclosure widgets can be opened at a time. As soon as one is opened, the browser will close the other. You can create exclusive groups through the name
attribute of <details>
. Having the same name
forms a semantic group:
<details name="faq-accordion">
<summary>Payment Options</summary>
<p>...</p>
</details>
<details name="faq-accordion">
<summary>Personalise your PIN</summary>
<p>...</p>
</details>
<details name="faq-accordion">
<summary>How can I add an additional cardholder to my Platinum Mastercard</summary>
<p>...</p>
</details>
Before using exclusive accordions, consider if it is helpful to users. If users are likely to want to consume more of the information, this will require them to open items often, which can be frustrating.
This feature is currently supported in all modern browsers so you can use it right away.
Styling the summary marker
A disclosure widget is typically presented with a small triangular marker beside it. In this section, we'll cover the process of styling this marker.
The marker is associated with the <summary>
element. The addition of the [::marker](https://developer.mozilla.org/docs/Web/CSS/::marker)
pseudo-element means that we can style the marker box directly. However, we are limited to a small set of CSS properties:
- All of the font properties
-
color
-
white-space
-
text-combine-upright
,[unicode-bidi](https://developer.mozilla.org/en-US/docs/Web/CSS/unicode-bidi)
, anddirection
properties -
content
- All animation and transition properties
As mentioned earlier, <summary>
is similar to a [<li>](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/li)
; it supports the list-style
shorthand property and its longhand properties. While this might sound a bit hodge-podge, it will be easier to understand the styling options with some examples.
Before jumping into examples, a quick word on browser support. At the time of writing, Safari is the only major browser that doesn’t fully support styling the marker:
- Safari support is currently limited to styling the
color
andfont-size
properties of the::marker
pseudo-element. Safari supports the non-standard pseudo-element::-webkit-details-marker
- Safari doesn’t support styling the
list-style
properties at all. See CanIUse for reference
Changing the color and size of a marker
Say we wanted to change the color of the triangular marker to red and make it 50% larger. We can do the following:
summary::marker {
color: red;
font-size: 1.5rem;
}
This should work across all browsers. Here’s the CodePen example.
Adjusting the spacing of the marker
By default, the marker is to the side of the text content of <summary>
and they are in the same bounding box. The list-style-position
is set to inside
. When it is an open state, the “additional information” is directly underneath the marker. Perhaps you want to change the spacing and alignment of this:
If we set list-style-position
to outside
, the marker sits outside of the <summary>
bounding box. This enables us to adjust the space between the summary text and the marker:
summary {
list-style-position: outside;
padding-inline-start: 1rem;
}
You can see this in the second instance in the screenshot above.
Here is a CodePen of this example:
Changing the marker text/image
If you want to change the content of the marker, you can use the content
property of the ::marker
pseudo-element. Based on your preferences, you can set it to text
. For my example, I used the zipper mouth emoji for the closed state and the open mouth emoji for the open state:
summary::marker {
content: '🤐 ';
font-size: 1.2rem;
}
details[open] summary::marker {
content: '😮 ';
}
To use an image for the marker, you can use the content
property of the ::marker
pseudo-element, or the list-style-image
property of <summary>
:
summary::marker {
content: url("arrow-circle-right.svg");
/* you can use a data URI too */
content: url('data:image/svg+xml,<svg height="1em" width="1em" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="white" d="M12 16.725L16.725 12 12 7.275 10.346 8.93l1.89 1.89h-4.96v2.36h4.96l-1.89 1.89zm0 7.087q-2.45 0-4.607-.93-2.156-.93-3.75-2.525-1.595-1.593-2.525-3.75Q.188 14.45.188 12q-.002-2.45.93-4.607t2.525-3.75q1.592-1.594 3.75-2.525Q9.55.188 12 .188q2.45 0 4.607.93 2.158.93 3.75 2.525 1.593 1.593 2.526 3.75.933 2.157.93 4.607-.004 2.45-.93 4.607-.93 2.157-2.526 3.75-1.597 1.594-3.75 2.526-2.154.932-4.607.93"/></svg>');
}
/* OR */
summary {
list-style-image: url("arrow-circle-right.svg");
}
In the following example, we are using two arrow icons from Material Symbols for the marker. The right-facing arrow is for the closed state, and the down-facing arrow is for the open state:
These examples will work as expected in Chrome and Firefox, but Safari will ignore the styles. You can approach this as a progressive enhancement and call it a day. But if you want the same appearance across all browsers, you can hide the marker and then add your own image as a stand-in. This gives you more freedom:
/* Removes default marker. Please consider accessibility, read below. */
summary::-webkit-details-marker {
display: none;
}
summary {
list-style: none;
}
You can visually indicate the state using a new marker icon, such as an inline image or via pseudo-elements. The <summary>
already (mostly) indicates the expand/collapse state. So if you use an inline graphic, it should be treated as decorative. An empty alt
attribute does this:
<!-- You can add your own image inside <summary> as a decorative element instead of the hidden marker. -->
<details>
<summary><img src="my-marker.png" alt>Do you want to know more?</summary>
<div>Yes</div>
</details>
You can choose to position the marker at the end of <summary>
, too, if you wish:
<!-- You can place it at the end of the summary text -->
<details>
<summary>Do you want to know more?<img src="my-marker.png" alt></summary>
<div>Yes</div>
</details>
However, it is important to note that hiding the marker causes accessibility issues with screen readers. Firefox, VoiceOver, JAWS, and NVDA all have an issue with consistently announcing the toggled state of the disclosure widget if the marker is removed. Unfortunately, the style is tied to the state. It is preferable to avoid doing this.
Styling the "additional information" section of <details>
You may want to style the "additional information" section of the disclosure widget without leaking styles to the <summary>
. Because you can have a variable number of elements inside a <details>
, it would be nice to have a catch-all rule:
<details>
<summary>Do you want to know more about styling the hidden section?</summary>
<h2>Styling hidden section</h2>
<p>Tell me more.</p>
<div>This is a div</div>
</details>
My go-to is to exclude the <summary>
element using the :not()
function. Just keep in mind that this targets each element rather than the content as a single section!
details > *:not(summary) {
color: palegoldenrod;
font-size: 0.8em;
margin-block-start: 1rem;
}
Alternatively, you can use the ::details-content
pseudo-element, which targets the entire section. This is why you want to use this for animating the opening and closing state transitions:
/* browser support is limited */
details::details-content {
color: palegoldenrod;
font-size: 0.8em;
margin-block-start: 1rem;
}
Notice the difference? There is only one margin at the start of the section. The <p>
and <div>
do not have margins. The downside of using the ::details-content
pseudo-element is that browser support is currently limited to Chrome.
Common mistakes when styling disclosure widgets
- Historically, it wasn't possible to change the
display
type of the<details>
element. This restriction has been relaxed in Chrome - Be careful changing the
display
type of<summary>
. The default isdisplay: list-item;
; if you change it todisplay: block;
, it may result in the marker being hidden in some browsers. This was an issue in Firefox:
/* This may cause the marker to be hidden in some browsers */
summary {
display: block;
}
- You cannot nest
<details>
-
Because the
<summary>
element has a default ARIA role ofbutton
, it strips all roles from child elements. Therefore, if you want to have a heading like a<h2>
in a<summary>
, assistive technologies such as screen readers won’t recognize it as a heading. Try to avoid this pattern:
<!-- h2 is not recognized as a heading by assistive technologies --> <details> <summary><h2>Spoilers</h2></summary> <ol> <li>Steven Spielberg shot the film in chronological order to invoke a real response from the actors (mainly the children) when E.T. departed at the end. All emotional responses from that last scene are real.</li> <li>When E. T. is undergoing medical treatment, an off-camera voice says, "The boy's coming back. We're losing E.T." The person delivering this line is Melissa Mathison, who wrote the screenplay for the film.</li> </ol> </details>
Hiding the marker causes accessibility issues with some screen readers. Firefox, VoiceOver, JAWS, and NVDA all have an issue with consistently announcing the toggled state of the disclosure widget if the marker is removed
Are there more changes to come?
Recently, there was a big proposal to help make <details>
more customizable and interoperable between browsers. Phase 1 includes some of what I covered in this article:
- Remove CSS
display
property restrictions so you can use otherdisplay
types likeflex
andgrid
- Specify the structure of the shadow tree more clearly. This should help with interoperability with Flexbox and CSS Grid
- Add a
::details-content
pseudo-element to address the second slot so that a container for the "additional information" in the<details>
element can be styled
The exciting news is items 1 and 3 in the list above have shipped in Chrome 131 (as of November 2024). The next phase should be tackling improving the styling of the marker. Additionally, there is a set of related changes that will help improve the ability to animate these elements.
Conclusion
The <details>
HTML element has gotten much easier to customize in CSS. You can now make exclusive groups with full browser support, animate the transition of opening/closing states as a progressive enhancement, and perform simple styling of the marker.
The Achilles’ heel of <details>
is the styling of the marker. The good news is that there is an active proposal that addresses this and some other pain points. This should remove all of the stumbling blocks when using <details>
. In the near future, you won’t need to write your own disclosure widget or use a third-party web component! 🤞
Is your frontend hogging your users' CPU?
As web frontends get increasingly complex, resource-greedy features demand more and more from the browser. If you’re interested in monitoring and tracking client-side CPU usage, memory usage, and more for all of your users in production, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording everything that happens in your web app, mobile app, or website. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.
Modernize how you debug web and mobile apps — start monitoring for free.
Top comments (0)