Accordion Expand
Native <details> panels that open and close smoothly instead of snapping. The content eases in and the plus turns into a cross.
Is it free?
Yes. Copy, paste and ship it.
Does it need a framework?
No. One script tag after anime.js.
Can I change the colours?
Every snippet has options.
How to use it
- Paste the snippet just before
</head>. - Add
data-accordion-expandto a<details>, or to a container of several to animate them all.
Options
Add any of these attributes to the same element.
data-single | On the container: opening one closes the others. |
data-duration | Time to open or close. Default 450 ms. |
Because it builds on <details>, it still works with the keyboard, screen readers and find-in-page. From code, animacious.accordion(details) toggles one.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Accordion Expand: native <details> panels that open and close smoothly
// instead of snapping, with the content easing in and a plus that turns to ×.
// Usage: <details data-accordion-expand><summary>Question?</summary><p>Answer.</p></details>
// or: <div data-accordion-expand data-single> …several <details>… </div>
// (data-single: opening one closes the others)
// Optional: data-duration (ms, default 450).
// Script use: animacious.accordion(detailsElement) toggles one from code.
var DEFAULTS = { duration: 450 };
var css =
'.acx>summary{list-style:none;display:flex;justify-content:space-between;align-items:center;gap:1em;cursor:pointer}' +
'.acx>summary::-webkit-details-marker{display:none}' +
'.acx-icon{flex:none;width:0.8em;height:0.8em;position:relative}' +
'.acx-icon::before,.acx-icon::after{content:"";position:absolute;left:0;right:0;top:50%;height:2px;margin-top:-1px;background:currentColor;border-radius:2px}' +
'.acx-icon::after{transform:rotate(90deg)}' +
'.acx-body{overflow:hidden}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function setup(det, group) {
var summary = det.querySelector('summary');
if (!summary) return;
det.classList.add('acx');
var icon = document.createElement('span');
icon.className = 'acx-icon';
icon.setAttribute('aria-hidden', 'true');
summary.appendChild(icon);
// Everything after the summary goes into a body we can animate the height of.
var body = document.createElement('div');
body.className = 'acx-body';
while (summary.nextSibling) body.appendChild(summary.nextSibling);
det.appendChild(body);
if (det.open) anime.utils.set(icon, { rotate: 45 });
det._acx = { body: body, icon: icon, group: group, busy: false };
summary.addEventListener('click', function (e) {
e.preventDefault();
toggle(det);
});
}
function toggle(det) {
var s = det._acx;
if (!s || s.busy) return;
var opening = !det.open;
var duration = +(det.dataset.duration || (s.group && s.group.dataset.duration)) || DEFAULTS.duration;
if (opening && s.group && s.group.dataset.single !== undefined) {
s.group.querySelectorAll('details.acx[open]').forEach(function (other) { if (other !== det) toggle(other); });
}
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
det.open = opening;
anime.utils.set(s.icon, { rotate: opening ? 45 : 0 });
return;
}
s.busy = true;
if (opening) det.open = true;
var h = s.body.scrollHeight;
anime.animate(s.icon, { rotate: opening ? 45 : 0, duration: duration, ease: 'outBack(1.6)' });
anime.animate(s.body, {
height: opening ? ['0px', h + 'px'] : [h + 'px', '0px'],
duration: duration,
ease: 'inOut(3)',
onComplete: function () {
s.body.style.height = '';
if (!opening) det.open = false;
s.busy = false;
}
});
anime.animate(s.body.children, {
opacity: opening ? [0, 1] : [1, 0],
y: opening ? [-6, 0] : [0, -6],
duration: duration * 0.8,
delay: opening ? duration * 0.2 : 0,
ease: 'out(2)'
});
}
function init() {
document.querySelectorAll('[data-accordion-expand]').forEach(function (el) {
if (el._acxInit) return;
el._acxInit = true;
if (el.tagName === 'DETAILS') setup(el, null);
else el.querySelectorAll('details').forEach(function (d) { setup(d, el); });
});
}
window.animacious = window.animacious || {};
window.animacious.accordion = toggle;
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-accordion-expand data-single>
<details>
<summary>Is it free?</summary>
<p>Yes.</p>
</details>
<details>
<summary>Do I need a framework?</summary>
<p>No.</p>
</details>
</div>