Updated answer:
The older version was a bit hacky. This is a better approach.
When you open the details element, its content - by definition - becomes visible. This is why every animation you apply is visible. However, when you close the details element, its content - by definition - becomes invisible, immediately. So even though every animation you try to apply will actually occur, it will simply not be visible.
So we need to tell the browser to suspend hiding the elements, apply the animation, and only when it's over, to hide the element. Let's use some JS for that.
We need to capture when the user toggles the details element. Supposedly, the relevant event would be toggle
. However, this event is fired only after the browser hides everything. So we will actually go with the click
event, which fires before the hiding happens.
const details = document.querySelector("details");
details.addEventListener("click", (e) => {
if (details.hasAttribute("open")) { // since it's not closed yet, it's open!
e.preventDefault(); // stop the default behavior, meaning - the hiding
details.classList.add("closing"); // add a class which applies the animation in CSS
}
});
// when the "close" animation is over
details.addEventListener("animationend", (e) => {
if (e.animationName === "close") {
details.removeAttribute("open"); // close the element
details.classList.remove("closing"); // remove the animation
}
});
@keyframes open {
0% { opacity: 0 }
100% { opacity: 1 }
}
/* closing animation */
@keyframes close {
0% { opacity: 1 }
100% { opacity: 0 }
}
details[open] summary~* {
animation: open .5s
}
/* closing class */
details.closing summary~* {
animation: close .5s
}
<details>
<summary>Summary</summary>
<p>
Details
</p>
</details>
Old answer:
const details = document.querySelector("details");
details.addEventListener("click", (e) => {
if (details.hasAttribute("open")) { // since it's not closed yet, it's open!
e.preventDefault(); // stop the default behavior, meaning - the hiding
details.classList.add("closing"); // add a class that applies the animation in CSS
setTimeout(() => { // only after the animation finishes, continue
details.removeAttribute("open"); // close the element
details.classList.remove("closing");
}, 400);
}
});
@keyframes open {
0% { opacity: 0 }
100% { opacity: 1 }
}
/* closing animation */
@keyframes close {
0% { opacity: 1 }
100% { opacity: 0 }
}
details[open] summary~* {
animation: open .5s
}
/* closing class */
details.closing summary~* {
animation: close .5s
}
<details>
<summary>Summary</summary>
<p>
Details
</p>
</details>
Note that time durations are not guaranteed to be accurate, not in CSS and not in JS. This is why I have set the JS timeout to be just a little bit lower than the CSS animation duration (400ms vs 500ms), so we are sure the animation ends just a bit after we're hiding the elements, so we don't get any flickering.