Animating Element with Only CSS
This post provides the most straightforward and minimum way to animate the HTML detials element using only CSS.
an you animate the opening and closing of the HTML <details> element using only CSS? Yes. Here’s the code:
details::details-content {
block-size: 0;
overflow: hidden;
transition-property: block-size, content-visibility;
transition-duration: var(--transition-duration, 0.2s);
transition-behavior: allow-discrete;
}
details[open]::details-content {
block-size: auto;
block-size: calc-size(auto, size);
}
It took an annoyingly long time to find a good answer to what felt like a basic question. Too many solutions relied on hacky approaches with obvious flaws, used unnecessary JavaScript, or only accomplished part of the solution (not animating the close).
I finally found what I was looking for in a video, and since I prefer blog posts that are easy to scan, find what one needs (especially when the author puts the answer at the top of the page!), and copy/pasted from, I decided to write this and share it for posterity.
Hope you found it useful. Please share.
Explanation
If you want a deeper understanding of what the code is doing, we can break it down line by line:
details::details-content {...}– This is the CSS selector for the content of the details element. In other words, this is the stuff we want to show and hide.block-size: 0;– Sets the size of the details content to zero.overflow: hidden;– Makes sure that as the element is animating between a height of0andauto, the content within it will not be shown.transition-property: block-size, content-visibility;– This animation relies on animating theblock-sizeANDcontent-visibilityproperties.block-sizeis the one that controls the animation of the height.content-visibilityis a little less obvious but is necessary because of how the<details>element works. It contains a Shadow DOM that controls the element under the hood and shows or hides the content visibility. We basically want thecontent-visibilityproperty to wait until theblock-sizehas finished animating before changing thecontent-visibility.transition-duration: var(--transition-duration, 0.2s);– Tells the browser how long the animation should take. In this case, we are assigning the value to the CSS custom property,--transition-durationwith a fallback of 0.2 seconds.transition-behavior: allow-discrete;– This is the line that makes it all work.transition-behavioris relatively new, andallow-discreteis the value that allows developers to animate ‘discrete’ properties. It’s too complicated to explain in full detail here, so check out the page ontransition-behaviorand Animatable CSS Properties on MDN.details[open]::details-content {...}– CSS selector for styling the details content only when the details element is open.block-size: auto;– A fallback for browsers that do not support thecalc-size()CSS function, which is a more modern approach for this solution.block-size: calc-size(auto, size);– Tells the browser to set the height of the details content to it.
Browse Support
This approach relies on some CSS that, at the time of publishing, does not work in Firefox. As a Firefox user, I am ok with this. This is an example of progressive enhancement where users on supported browsers will get the animated experience, and users on non-supported browsers get the default. The <details> element still opens and closes as expected, it just does not animate.
This is an affordance I am ok with because:
- Eventually, more browsers will catch up.
- It uses built-in browser features as they are intended, without relying on hacks.
- I don’t have to remember at some point later on to remove code.
Bonus: Custom Marker & Animation
It’s also possible to use a custom marker and animate it on open and close. This example removes the default ::marker pseudoelement and replaces it with a custom » character using the ::before pseudoelement. It also relies on some absolute positioning and padding, so you might need to tweak it a bit
summary {
position: relative;
padding-inline-start: .75rem;
}
summary::marker {
content: '»';
content: none;
}
summary::before {
content: '»';
position: absolute;
inset-inline-start: 0;
inset-block-start: -0.05rem;
transition: rotate 0.2s;
}
details[open] summary::before {
rotate: 90deg;
inset-block-start: 0.05rem;
}
Credits
The best solution found came in a video by CSS Weekly‘s Zoran Jambor. His demo is much fancier, and he goes into much more details explaining how things work and why he took this approach. I highly recommend watching it and following his work.
Thanks for stopping by my corner of the internet. Here, there are no ads, trackers, paywalls, accounts, etc. It's hard, but worth the better experience. If you'd like to keep this site up, consider hiring me, making a donation, buying something (soon), leaving a comment, or sharing. It helps more than you know. I'm glad you're here :)
Did you enjoy this article?
Recommend it — Standard Reader surfaces well-loved writing to more readers across the network.