Animated details + summary


(Last modified: )

Cross browser animation of details and summary elements using CSS grid and JavaScript.

Read more about this at Animate <details> Element when Expanding or Collapsing

<details>
    <summary>Summary</summary>
    <div>
        <div>
            <!-- your content -->
        </div>
    </div>
</details>
details > div {
    overflow: hidden;
    display: grid;
    /* intentionally independent from .animation as Safari 16
    would otherwise ignore the expansion animation. */
    animation-duration: 0.2s;
}

details > .animation {
    animation-name: grid-expand;
    animation-timing-function: ease-out;
}

details > .collapsing {
    animation-direction: reverse;
    animation-timing-function: ease-in;
}

details > div > div {
    min-height: 0;
}

@keyframes grid-expand {
    0% {
     grid-template-rows: 0fr;
    }
    100% {
        grid-template-rows: 1fr;
    }
}
document.querySelectorAll('summary')
    .forEach(element => element.addEventListener('click', (event) => {
        const detailsElement = event.target.parentElement;
        const contentElement = event.target.nextElementSibling;

        // Chrome sometimes has a hiccup and gets stuck.
        if (contentElement.classList.contains('animation')) {
            // So we make sure to remove those classes manually,
            contentElement.classList.remove('animation', 'collapsing');
            // ... enforce a reflow so that collapsing may be animated again,
            void element.offsetWidth;
            // ... and fallback to the default behaviour this time.
            return;
        }

        const onAnimationEnd = cb => contentElement.addEventListener(
            "animationend", cb, {once: true}
        );

        // request an animation frame to force Safari 16 to actually perform the animation
        requestAnimationFrame(() => contentElement.classList.add('animation'));
        onAnimationEnd(() => contentElement.classList.remove('animation'));

        const isDetailsOpen = detailsElement.getAttribute('open') !== null;
        if (isDetailsOpen) {
            // prevent default collapsing and delay it until the animation has completed
            event.preventDefault();
            contentElement.classList.add('collapsing');
            onAnimationEnd(() => {
            detailsElement.removeAttribute('open');
            contentElement.classList.remove('collapsing');
            });
        }
    })
);