Chapter
Animating the details element
Outline:
- Styling
- Changing the marker. Does it still affect accessibility announcements?
- Animating the marker
- Animating the opening and closing using CSS
Resources:
- Animate HTML Details & Summary Elements Using Pure CSS
- Animate details and summary with a few lines of CSS
- https://css-tricks.com/using-styling-the-details-element/
- Animated accordion recipe https://nerdy.dev/notebook/accordion.html#<details>-accordion-recipe
opacity (continuous) and display (discrete).
Related CSS features:
The ::marker pseudo-element
The ::details-content pseudo-element (represents the expandable content of the details element https://drafts.csswg.org/css-pseudo-4/#details-content-pseudo)
The transition-behavior property
The interpolate-size property
The list-style-position: outside property
Animating to from height auto means animating from a known number to an unknown number values determined by children. This is what we need interpolate-size: allow-keywords for. Keywords include auto, fit-content, etc.
Whereas animating to from display: none and visibility: hidden requires transition-behavior: allow-discrete because display and visibility/content-visibility are discrete states that don’t really animate
opacity (continuous) and display (discrete).
Display values are discrete and may be defined as: inline, inline-block, flex, grid, inline-flex, inline-grid. All those properties will result in visible content.
Display may also be set to none, hiding the content entirely.
Historically, CSS has not had a way to animate from a point A of display: block to a point B of display: none or of a point A of display: none to a point B of any visible display type, such as display: grid.
we apply the transition-behavior of allow-discrete to all elements. The transition-properties we’re using are visibility, transform, and opacity. Opacity and transform are both continuous properties that have been able to transition since transition was implemented in CSS. Visibility is a discrete property and needs allow-discrete to transition properly.
Introduction:
HTML is where meaning is carried and conveyed to assistive technologies, and where the majority of your accessibility efforts will typically go.
CSS is a layer on top of HTML. Its purpose is to provide visual styles for the content you create in your markup.
CSS affects the visual appearance of an element, and it can affect the visibility and availability of an element, too. Because of that, it can have a direct impact on the accessibility information that the browser exposes in the accessibility tree. We’ve already hinted at this in the accessibility tree chapter, and we will talk much more about this in the next chapter on hiding content. We will also discuss how CSS pseudo-elements contribute to the accessible name computation of an element in the chapter on techniques for providing accessible names.
In this chapter, we will discuss how we can use CSS to enhance the native <details> disclosure widget by animating the opening and closing of this widget. We will also discuss how to style the marker for the <summary> of the widget, and how doing so could potentially affect the accessibility of the widget.
Animating the opening and closing of the <details> element has long been a source of frustration to us.
In this chapter, we’re going to learn how to use modern CSS features to enhance the native HTML disclosure widget with a nice, smooth transition using just a few lines of CSS and no JavaScript.
Animating the opening and closing of the details disclosure widgets with CSS only
By default, the browser does not animate the opening and closing of the <details> disclosure widget. It reveals and hides the contents of the widget but it does not do so with a smooth transition between the expanded and collapsed states.
Previously, we’ve had to resort to using JavaScript to animate the opening and closing of the <details> element because CSS did not allow animating or transitioning to and from display: none, visibility: hidden, and height: auto.
Today, modern CSS features make it possible to animate the contents of the <details> element using CSS only.
To do that, you will want to use the calc-size() function and the allow-discrete value of the transition-behavior property with the ::details-content pseudo element.
Some of the features we’re going to discuss and use are very new and not yet supported across browsers. But that won’t stop us from using them because the great news is that they are designed to be used as an enhancement. In other words, if the browser does not support any of these features, it’ll just fall back to opening and closing the <details> element without a transition, which is still perfectly usable.
A quick overview of the CSS features we’ll use
The ::details-content pseudo-element is an element-backed pseudo-element, as per the specification.
The element-backed pseudo-elements, interact with most CSS and other platform features as if they were real elements (and, in fact, often are real elements that are not otherwise selectable).
Think of the ::details-content element as representing an imaginary box surrounding the contents of the details. This element is a sibling of the <summary> inside the widget; it does not contain the summary—it contains all the content that follows it.
This box is, by default, not rendered in and of itself. The browser applies display: contents to this box
By default, ::details-content (shown in Shadow DOM) has display:block; content-visibility: hidden set on it. It also has display: contents set on it, which means it doesn’t have a rendered box itself.
==try overflow:hidden on details-content vs on details element==
:root {
interpolate-size: allow-keywords; /* this is what makes animating to height:auto possible */
}
/* target the pseudo-content part of the details element; this part follows the summary in the DOM */
details::details-content {
display: block; /* overrides the default display: contents so that we can apply styles to it (like padding and margin) */
block-size: 0;
transition: all .6s allow-discrete; /* all: block-size, and content-visibility which is set on the details-content by default! This is why we need allow-discrete transition-behavior value. */
}
details[open]::details-content {
block-size: auto; /* calc-size() can be used insetad which also implicitly applies interpolate-size: allow-keywords as well */
}
==block-size is the logical property equivalent of the height property. inline-size is the logical property equivalent of width==
Discrete-animated properties generally flip between two values 50% through animating between the two.
There is an exception, however, which is when animating to or from
display: noneorcontent-visibility: hidden. In this case, the browser will flip between the two values so that the transitioned content is shown for the entire animation duration.
And since details-content has content-visibility: hidden on it, we want to tell the browser to animate that too. We do that using transition-behavior: allow-discrete.
==content-visibility hidden means element still has a generated box. so if you apply padding to details-content you can end up with white space. Kevin Powell’s video does. BUT it shouldn’t take padding at all because it’s display:contents, as Zoran’s video shows. Check==
Styling the marker
The ::marker pseudo-element ==TODO: check PR about allowing more styling)
summary::marker {
list-style-position: outside; /* prevent text from wrapping under the arrow on smaller widths */
content: none;
/* OR */
opacity: 0;
}
summary::before {
/* new marker here.
TODO: test how this affects SR announcements in FF and others.
OR leave the default, make it transparent, and use an empty alt for the pseudo-element here! */
content: url(...) / '';
}
==go more over styling markers and list-styles==
::details content
One is the ::details-content pseudo-element. This is a part of the
<details>element’s shadow tree — yes, many HTML elements are implemented with the Shadow DOM.::details-contentis a part-like pseudo-element that exposes the internal container element. In Chromium that container element is<slot>which defaults todisplay: contents,::details-contentsets the element asdisplay: blockand then hides the content withcontent-visibility: hidden. This means you can both style the element that houses the content and more easily animate it. —How content-visibility is used https://12daysofweb.dev/2024/css-content-visibility/
From MDN:
The interpolate-size CSS property allows you to enable animations and transitions between a <length-percentage> value and an intrinsic size value such as auto, fit-content, or max-content. ==it makes animating to height:auto possible==
This property is typically used to animate the width and/or heightof a container between a <length-percentage> and the full size of its content (i.e., between “closed” and “open” or “hide” and “reveal” states) when animating a non-box-model CSS property, such as transform, is not a viable solution.
Setting interpolate-size: allow-keywords enables interpolation between a <length-percentage> value and an intrinsic size value. Note that it does not enable animating between two intrinsic size values. One end of the animation must be a <length-percentage>.
:root {
interpolate-size: allow-keywords;
}
In effect, including calc-size() in a property value automatically applies interpolate-size: allow-keywords to the selection. ==I’ve found that not to be true in practice! I needed to add it otherwise the transition didn’t work on details (tested in Edge). it seems calc-size() doesn’t work or do what it should do in Edge just yet==
See the Pen Untitled by Sara Soueidan (@SaraSoueidan) on CodePen.
Continue learning
- [ ] A video about interpolate-size and calc-size (or text-version: https://12daysofweb.dev/2024/calc-size-and-interpolate-size/)
Styling the summary marker with CSS
Browsers provide a default triangle icon marker for the <summary> element to visually indicate the current state of the widget (whether it is expanded or collapsed).
If you want to customize the look of your widget, you may want to replace this marker with a different one.
To provide your own marker, you will want to remove the default triangle and then replace it with your own icon.
Now, as we mentioned earlier, some browser and screen reader pairings announce the default triangle marker as part of the <summary>'s accessible name.
Firefox in particular exposes the default triangle marker as part of the <summary>’s accessible name. So if you use VoiceOver, JAWS or NVDA with Firefox, the screen reader would announce “filled down-pointing triangle…” and “filled right-pointing triangle…”, followed by the <summary>'s label. (The exact wording for the triangle varies depending on which screen reader and platform you used.)
This is generally undesired behavior, and there is a Firefox bug filed for it.
That’s not the whole story, though.
Not long ago, if you removed the default marker so you can insert your own custom chevron or other visual expanded/collapsed state marker, the state of the widget became unclear with Firefox and VoiceOver, since the announcement of the default triangle direction was the only way state is communicated in that pairing! JAWS and NVDA also had an issue with consistently announcing the toggled state of the disclosure widget if this marker is removed.
What this means is that: in Firefox, the semantics of the <summary> element were semi-reliant on its default styling.
Fortunately, times have changed!
Today, at the time of making of this chapter, Firefox still exposes the triangle in the <summary>'s accessible name; but the state of the widget is no longer reliant on it. VoiceOver, JAWS, and NVDA will append and announce the state of the widget after the accessible name.
So, you can safely remove the default marker and replace it with your own marker today.
Nonetheless, the previous behavior serves as a goood reminder of how important it is to always test your work in different browser and screen reader pairings, especially once you start customizing native elements with CSS, as it may sometimes have unintended and unexpected side effects.
Providing a custom marker
To provide your own marker, you will need to:
-
Hide the default marker. There are a couple of ways to do that in CSS today.
-
Provide a new marker and hide this marker from screen readers so that it is not announced as part of the
<summary>'s accessible name.
You can provide a new marker either using a CSS pseudo-element, or using an inline SVG. We’re going to discuss how to hide the marker from screen readers in both cases.
First, let’s start by removing the default marker.
Removing the default triangle marker
To remove the default triangle marker, you can use the list-style property on the <summary> element and set its value to none.
summary {
list-style: none;
}
This is similar to how you remove default list (<ul>, <ol>) markers, and it works across all major browsers today.
Alternatively, you could also use the ::marker pseudo-element, which selects the marker box (the box containing the marker) of a list item.
Using the ::marker pseudo-element, you can modify the default marker on the the <summary>. You do that by setting the value of the marker you want in the content.
So, to remove the marker with the ::marker pseudo-element, you can set the content property on it either to none or to an empty string:
summary::marker {
content: none;
/* or */
content: "";
}
Providing a custom marker using CSS and hiding it from screen readers
To provide your own marker to the <summary> in CSS, you can once again use the ::marker pseudo-element—in which case you would be using it to replace the default marker, not to remove it.
summary::marker {
/* This replaces the default marker with the image.svg */
content: url(../../image.svg);
}
Alternatively, you can use the CSS ::before (or ::after) pseudo-element:
summary {
list-style: none;
}
summary::before {
content: url(../../image.svg);
}
And finally, whether you use ::marker or ::before (or ::after), you will want to hide the marker from screen readers.
By default, CSS pseudo-content is exposed to screen readers and announced to the user.
Not only that, but CSS pseudo-content will contribute to the accessible name computation of the element when the element’s name is derived from its contents (as is the case for our <summary>). (We will talk more about this in the accessible naming techniques chapter.)
Screen readers already announce the expanded/collapsed state of the disclosure widget. The state is baked into the semantics of the <details> element and is (thankfully) not tied to the marker.
The marker is a purely visual state indicator and does not convey any meaningful semantics to screen reader users. So, announcing it is unnecessary—even undesirable.
To hide the CSS marker from screen readers, you should give it an empty alternative text.
Just like an empty alt text on an <img> element will cause the image to be treated as decorative and will hide the image from screen readers (which is something we will talk more about in the accessible Images chapter), providing empty alt text for CSS-generated content hides the content from screen readers.
To give the marker an alternative text in CSS, you can use the content property syntax defined in the CSS Generated Content Module Level 3.
As stated in the specification, the content property now accepts alternative text to be specified after a slash (/) following the image.
So, for the marker, the CSS would look like this:
summary::marker {
content: url(../../image.svg) / "";
}
/* OR */
summary {
list-style: none;
}
summary::before {
content: url(../../image.svg) / "";
}
The empty alternative text indicates to the browser that this image is meant to be decorative and should, therefore, be excluded from the accessibility tree and not be exposed to screen readers.
CSS-generated markers are very convenient to create. But, depending on the marker, they can have some limitations.
If you reference an external image to be used as your marker, you will need to adapt the image to forced colors modes such as Windows Contrast Themes. To do that, you can use the CSS forced colors feature query, which checks for whether forced colors mode is currently active on the user’s machine:
EXAMPLE
We will learn more about this feature query in the forced colors mode chapter.
That being said, one very important aspect of forced colors mode is that you can never know what colors are being used in the theme. The feature query checks whether forced colors mode in active, but the user might be using any color palette of their choice (they can even customize the default themes provided by the browser and choose low-contrast colors instead!), and there is no way you can query which colors are currently being used. So the colors of the marker you create may clash with the colors of the color scheme chosen by the user, making it very difficult (if not impossible) to perceive.
So, ideally, you’ll want to provide a marker that automatically adapts to—and makes use of—the colors the user’s operating system.
An inline SVG is ideal for this purpose.
Providing a custom marker using SVG and hiding it from screen readers
Inline SVG is probably the most flexible way to provide adaptive icons and markers to your components.
Instead of providing the marker in CSS, you embed the marker as an inline <svg> in your markup, inside the <summary> element:
<details>
<summary>
<svg viewBox=".." width=".." height=".." aria-hidden="true">
..
</svg>
<span>[ Disclosure widget trigger label ]</span>
</summary>
</details>
Because the SVG marker provides no semantic meaning that is required by screen readers to convey the state of the widget, you can hide it from screen readers using the aria-hidden attribute. (We will learn all about aria-hidden, as well as other methods for hiding content inclusively, in the hiding techniques chapter.)
Now, the advantage of using an inline SVG here is the ability to select and style the SVG and its contents from your CSS.
Styling the SVG content from your CSS means that you can use CSS color keywords (like system color keywords) to provide a fill color for the SVG (or its content).
For example, suppose you’re using this SVG image which contains a <path> representing a custom chevron:
<svg viewBox="0 0 24 24" width="1em" height="1em">
<path d="M8.586 5.586c-0.781 0.781-0.781 2.047 0 2.828l3.585 3.586-3.585 3.586c-0.781 0.781-0.781 2.047 0 2.828 0.39 0.391 0.902 0.586 1.414 0.586s1.024-0.195 1.414-0.586l6.415-6.414-6.415-6.414c-0.78-0.781-2.048-0.781-2.828 0z"></path>
</svg>
Using CSS, you can select the SVG and specify the fill color you want to apply to the icon.
In this example, I am using the canvasText system color keyword to set the fill color of the icon:
@media screen and (forced-colors: active) {
svg {
fill: canvasText; /* icon's fill color inherits the color of its surrounding text */
}
}
System color keywords implicitly map to whatever color values the user has chosen.
The canvasText system keyword sets the fill color of the SVG to inherit the color of its surrounding text, which is specified in the forced-colors palette chosen by the user.
As we mentioned earlier, we will learn lot more about forced colors modes in the forced colors mode chapter.
It’s worth mentioning that you can also use the currentColor keyword, which always inherits the value of the color property.
svg {
color: inherit;
fill: currentColor;
}
So if you set the fill color of the SVG to inherit the color of the surrounding text, it will adapt to whichever color is being used in high contrast mode.