Building an animated <details> element
Accordion elements on the web seem to be everywhere and almost everyone seems to implement them in a way that only works for the well sighted user with a modern browser with JavaScript enabled. I want to show that these can be built in a better way.
Table of Contents
So why are accordions used at all?
While using accordions or expanders in some situations is a bad idea, sometimes they help with uncluttering, hiding information that isn't needed by most people but available from the already loaded document on request when needed.
An example would be to provide contact details on a page, usually visitors don't care, but when they do, that information is easy to find.
Another example would be an event log with the summaries always being visible and the details being available inline on request. sourcehut and YunoHost use this pattern.
Note: Accordions usually are multiple collapsible elements grouped together and only allow one open at a time. I'm not going into that functionality here.
The HTML way of building an accordion …
This pattern is so useful that HTML has elements for exactly that usecase, the <details>
and <summary>
elements, one simply puts a summary inside a details tag along with some content and it works, in all browsers, without JavaScript, with any input method the browser supports.
Note: Apparently the details element and accessibility is it's own can of worms, too. As the article mentions: You decide.
Your DNS records for example.org are not configured
Add an A record pointing to 93.184.216.34 .
Add an AAAA record pointin to 2606:2800:220:1:248:1893:25c8:1946
It is important to set both, otherwise peopley may be unable to access your server.
It then would be displayed similar to this:
Your DNS records for example.org are not configured
Add an A
record pointing to 93.184.216.34
.
Add an AAAA
record pointing to 2606:2800:220:1:248:1893:25c8:1946
It is important to set both, otherwise people may be unable to access your server.
What people are doing instead …
You may have been surprised by the fact that there is an HTML element for exactly this purpose because you have never seen one in the wild.
There are probably three reasons people don't use it:
- They simply don't know that it exists
- They found out, tried to animate it and failed to do so
- They use a library which fell victim to one of the previous reasons.
The result being that they give up and write their own accordion from scratch, button onclick
handler, element with changing styles, done. Except that the result is usually an accessibility nightmare, and completely falls apart when for some reason the JavaScript needed to operate it isn't available.
Why is the details element seemingly hard to animate?
Animating a details tag is simple, one can use the [open=""]
CSS selector to find out whether the details element is currently open, put some keyframes and an animation (more details later) on it and it opens with a smooth animation and closing is … not animated at all.
No your CSS isn't at fault here.
How the details element works.
The details element is a bit of a unicorn in that it manipulates the page tree. When the content gets shown elements are added and when it closes the elements get removed from the page instantly, and while that is great as it saves resources PR is not happy because they want an animation.
My personal guess is that this is where most developers who know about the details element give up.
Animating it Anyway
So we want an animation for sighted people with modern browsers and a good experience for everyone else, so lets try to achieve that by using the web as it was intended, HTML for content, CSS for eye candy and JavaScript for providing some optional functionality.
Note: Animating slightly degrades accessibility (which is probably still better than with your completely custom accordion) of the details element.
You can find the final result over on Codeberg.
Let's start with the content, I'll assume you have a plain details and summary tag filled with your content and we want to leave it as it is.
Your DNS records for example.org are not configured
Add an A record pointing to 93.184.216.34 .
Add an AAAA record pointin to 2606:2800:220:1:248:1893:25c8:1946
It is important to set both, otherwise peopley may be unable to access your server.
In case you want to follow along but only have a desktop browser: open a new tab, navigate to about:blank
and open up the developer tools. Depending on the browser you now have a pretty good IDE with live preview.
The Grand Opening
Before we worry about closing the element, lets worry about it opening.
The simplest way here is to simply animate the max-height
property from the current height of the element (--details-current-height
) to one screen height (100vh
), filling only back in time in case the details element is taller than one screen height (i.e. on a smaller device), in that case it will simply snap to full height off screen and is fully viewable.
Because we currently have no idea what the current height of the details element is, falling back to an assumed 1em
as the height of our summary element works pretty well. (You may have to adjust this based on extra padding and margin you are applying)
{
from }
to }
}
}
With that the opening part is taken care of.
Fading out of Existence
Because the details element immediately closes and there is no way to animate that we have to put a delay between the actual interaction with the element and the open
attribute being removed, so let's add some JavaScript.
Because we want to hook on the interaction of a sighted user with the summary element, we use an onclick
handler here and assume a details_click_handler()
function.
The following function simply adds the onclick
handler to every summary element on the page (if you only want to target specific elements … I'll leave that as an exercise).
To run this initalizer you can use any method you like, but since in my experience document.onload
is a bit wonky when testing inline code on small documents here is a way to defer it until the first interaction.
document.onclick =
With that set up we can now focus on the click handler, which has to do two things:
- add a style-class that will trigger the animation
- set a timer to actually close the element after the animation.
details_click_handler
With the bare minimum of JavaScript added let's add some CSS to get a proof of concept animated details element. This is basically the reversed opening from the current height, defaulting to 100vh
to the height of the summary element defaulting to 1em
again.
Again we'll worry about actually setting those variables to something useful later.
{
from }
to }
}
}
Now you should have a not pretty but animated closing details element.
Improving the Animation
Now that we have a proof of concept, lets make it usable.
The issues we currently have are:
- Handle an interaction while the animation is playing correctly.
- Accessibility impact is larger than it could be.
- The heights used are still placeholders.
- The animation speeds don't match up.
Handling an interaction while the animation is playing
Currently when one interacts while the animation is playing the behaviours is that the element keeps closing, which is not great.
To fix it we have to add some additional checks to our handler function to test if the closing
class is present on an open element. If that's the case we just remove the class gain instead of launching a new timeout and on the timeout part we just discard the timeout if the closing class isn't present.
Lowering the impact on accessibility
There are two problems I'm trying to solve with this one:
- When only relying on something that uses the accessibility interface of the browser (i.e. screenreader only) there is an inexplicable delay between interaction and the element being announced as collapsed, very confusing and/or annoying.
- When navigating by keyboard, no matter what the output device is, the content inside the details element can still be focused while the animation is playing, again potentially confusing and/or annoying.
Each of the alone would be enough of a reason (at least for me) to not animate the closing of the element on keyboard interaction. (Fixing this is probably possible but at that point we are writing a full blown expander in JavaScript.)
Based on this we make a simple assumption: If you are not using a pointing device, you are probably not in the group that greatly benefits from an animation.
Note: Of course the "uses a pointer" is a heuristic that will be sometimes be wrong.
To achieve this one can test for the offsetX
and offsetY
values of the event to find out weather the event came from a pointer or not, both are 0 they probably didn't come from a pointer and we tell the browser to do what it would do without us interfering.
To stop the opening animation from playing when we won't deliver a corresponding closing animation we add an animated
class as an additional requirement in our two animation CSS rules.
This way we can make PR and our bosses happy while mostly staying out of the way of people who appreciate clean websites more than fancy animations.
Note that after this the animation won't play if the JavaScript isn't running (The element will just snap open and closed like at the start).
Deglitching the animation
Now that we have taken care of accessibility issues, we can put the CSS variables introduced earlier to use to make the animation responsive to the current state to avoid some glitches.
This means populating the --details-summary-height
and --details-current-height
properties directly on the details elements.
;
;
'--details-summary-height', summary_height+"px";
'--details-current-height', current_height+"px";
Getting the timings to match
After that is fixed the last open issue is, that the speed of the animation opening and the speed of the animation closing don't match.
With the opening speed currently being one screen height minus summary per second it is pretty easy to get the closing animation to match that as when the accordion is open we can just ask the browser how tall the details and summary elements are, how tall the screen is and calculate the animation length from that. Since we already know the heights of both relevant elements we'll use the variables from earlier.
closing
CSS class
;
'--details-close-animation-length', close_animation_duration+"s";
To make the actual state match the timeout previously set to 500
should now be close_animation_duration*1000+10
the extra 10ms is to allow the browser a bit of breathing room so that we don't cut the animation off before it has finished.
The final result
The resulting details_click_handler
function now should look like the following.
And our final CSS:
{
from }
to }
}
{
from }
to }
}
}
}
Updates
2023-03-25
- Added note about accordions usually having a grouping functionality.
- Added link to a [great article by Scott O'Hara explaining the problems around how details element is implemented in browsers].
- Added explanation why I'm disabling the animation for non-pointer inputs here.