← Overlays

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

  1. Paste the snippet just before </head>.
  2. Add data-accordion-expand to a <details>, or to a container of several to animate them all.

Options

Add any of these attributes to the same element.

data-singleOn the container: opening one closes the others.
data-durationTime 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.

1 · Snippet
<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>
2 · Your HTML
<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>