Short answer
You cannot make the native <details> open/close animation stable, because the HTML standard does not define one. It specifies the open attribute and the toggle/beforetoggle events; the visual transition is left to the user agent. Author CSS outranks the UA stylesheet for properties you can name, but it cannot pin behavior that lives inside the engine. The durable approach is to keep <details>/<summary> for semantics and animate an inner wrapper yourself.
Confirmed facts vs. likely explanation
Confirmed: the standard leaves the transition unspecified; when closed, the content box is not rendered, so descendant transitions never run on collapse; author styles beat UA styles for the same property; toggle fires after the state change, while beforetoggle is cancelable.
Likely, and worth re-checking against current releases: the specific Chromium fade, Firefox's lack of animation, and the Safari 14-to-15 shift in your premise. Treat those as historical observations rather than constants — that is precisely why you should not build on them.
Why transitions on <details> alone fail
Opening is easy: the content appears and a transition can run. Closing is the hard part — the content is removed from rendering, so there is nothing left to interpolate. A transition declared on details or its descendants cannot animate a box that no longer exists. That asymmetry, not a browser bug, is the core limit.
Stable pattern
- Reset UA animation on the elements you can target, and hide the native marker.
- Wrap the body content in a single inner element.
- Drive a class from JavaScript, using
beforetoggle for exit and toggle for entry.
details, summary { animation: none; transition: none; }
summary { list-style: none; }
summary::-webkit-details-marker { display: none; }
.panel {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 200ms ease;
}
details[data-anim="open"] .panel { grid-template-rows: 1fr; }
.panel > div { overflow: hidden; min-height: 0; }
@media (prefers-reduced-motion: reduce) {
.panel { transition: none; }
}
Minimal state driver — keep open as the source of truth:
const d = document.querySelector('details');
d.addEventListener('toggle', () => {
d.dataset.anim = d.open ? 'open' : 'closed';
});
For an animated collapse you must delay the removal of open: either wait for transitionend on the panel with a timeout fallback, or call preventDefault() in beforetoggle and manage open yourself. The second route is more code and more risk — if you take it, you own ARIA state and keyboard behavior.
Declarative alternative
Recent CSS can express this without JavaScript: transition-behavior: allow-discrete on display/content-visibility, @starting-style, and interpolate-size for height: auto. Support is uneven, so feature-detect and keep the JavaScript path as a fallback.
CSS.supports('selector(details::details-content)');
CSS.supports('transition-behavior', 'allow-discrete');
CSS.supports('interpolate-size', 'allow-keywords');
Regression testing when browsers update
- Assert computed
animation-name, transition-property, display and content-visibility on details, summary and ::details-content before and after toggling.
- Log
element.getAnimations() and the order of beforetoggle/toggle; UA animations appear there even with no author CSS.
- Run screenshot diffs in Chromium, Firefox and WebKit at fixed delays after toggle, and re-baseline after each browser upgrade.
- Re-test with
prefers-reduced-motion: reduce and with keyboard-only interaction to confirm open still reflects real state.
One detail that changes the recommendation
Do you need an animated collapse, or only an animated open? If only open, a pure-CSS class toggle is sufficient and you can skip the beforetoggle machinery. If collapse must animate, you need the delayed-removal or preventDefault path, and the testing burden grows accordingly.