← Navigation

Sidebar Collapse

Press the toggle and the sidebar shrinks to a slim rail of icons, its labels fading out one after another as the content beside it spreads into the space. Press it again and it widens and the labels slide back in.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-sidebar-collapse to your sidebar. Each child is an item: an icon (an svg, img or i) followed by its text.
  3. The script wraps the text so it can fade, and adds a Collapse button at the bottom. Style it with .sbc-toggle, or mark one of your own children data-toggle to use that instead.

Options

Add any of these attributes to the same element.

data-collapsedWidth when collapsed, in px: the icon plus the padding round it. Default 64.
data-startSet to collapsed to start slim.
data-durationHow long it takes to fold. Default 450 ms.
data-autoplayIt collapses and expands by itself while in view, pausing when someone taps. Handy for demos.

Open, it is as wide as your CSS makes it. Collapsed, the sidebar has the class sbc-collapsed and each item keeps its name as a tooltip and an aria-label. It fires a toggle event (e.detail.collapsed), and animacious.sidebarCollapse(aside) folds or unfolds it from code.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Sidebar Collapse: press the toggle and the sidebar shrinks to a slim rail
    // of icons while its labels fade out one after another; press it again and
    // it widens and the labels slide back in.
    // Usage: <aside data-sidebar-collapse>
    //          <a href="/"><svg>…</svg>Home</a>
    //          <a href="/inbox"><svg>…</svg>Inbox</a> …
    //        </aside>
    // Each child is an item: an icon (svg, img or i) followed by its text. The
    // script wraps the text in a span (class "sbc-label") and adds a collapse
    // button at the end (class "sbc-toggle"), unless a child has data-toggle,
    // which is then used instead.
    // Optional: data-collapsed (width when collapsed, in px, default 64; make it
    // fit the icon plus the padding round it), data-start="collapsed" (start
    // slim), data-duration (ms, default 450), data-autoplay (it collapses and
    // expands by itself while in view; handy for demos).
    // The sidebar gets the class "sbc-collapsed" while slim and fires a "toggle"
    // event (e.detail.collapsed). Collapsed items keep their name as a tooltip.
    // Script use: animacious.sidebarCollapse(aside) collapses or expands it.
    var DEFAULTS = { collapsed: 64, duration: 450, stagger: 25 };
    var CHEVRON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 6l-6 6 6 6"/></svg>';

    var css =
      '.sbc{display:flex;flex-direction:column;overflow:hidden;box-sizing:border-box;flex:none}' +
      '.sbc>*{display:flex;align-items:center;white-space:nowrap;flex:none}' +
      '.sbc>*>svg,.sbc>*>img,.sbc>*>i{flex:none}' +
      '.sbc-label{display:inline-block}' +
      '.sbc-toggle{margin-top:auto;font:inherit;color:inherit;background:none;border:0;cursor:pointer;-webkit-tap-highlight-color:transparent;text-align:left}' +
      '.sbc-chev{display:inline-grid;place-items:center;flex:none}' +
      '.sbc-chev svg{width:100%;height:100%;display:block}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    function apply(el, collapsed, still) {
      var s = el._sbc;
      s.collapsed = collapsed;
      el.classList.toggle('sbc-collapsed', collapsed);
      if (s.a11y) {
        s.toggle.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
        s.toggle.setAttribute('aria-label', collapsed ? 'Expand sidebar' : 'Collapse sidebar');
      }
      s.items.forEach(function (it) {
        if (collapsed && it.text) it.el.title = it.text; else it.el.removeAttribute('title');
      });
      anime.utils.remove([el, s.chev].concat(s.labels));
      // Open, the sidebar is as wide as its CSS and contents make it.
      var was = el.style.width;
      el.style.width = '';
      var full = el.offsetWidth;
      el.style.width = was;
      var w = collapsed ? s.slim : full;
      var settle = function () { if (!s.collapsed) el.style.width = ''; };
      if (still) {
        el.style.width = collapsed ? w + 'px' : '';
        anime.utils.set(s.labels, { opacity: collapsed ? 0 : 1, x: collapsed ? -8 : 0 });
        anime.utils.set(s.chev, { rotate: collapsed ? 180 : 0 });
        return;
      }
      var dur = s.duration;
      anime.animate(s.chev, { rotate: collapsed ? 180 : 0, duration: dur, ease: 'inOut(3)' });
      if (collapsed) {
        anime.animate(s.labels, { opacity: 0, x: -8, duration: dur * 0.45, delay: anime.stagger(DEFAULTS.stagger), ease: 'out(2)' });
        anime.animate(el, { width: w, duration: dur, delay: dur * 0.2, ease: 'inOut(3)' });
      } else {
        anime.animate(el, { width: w, duration: dur, ease: 'out(4)', onComplete: settle });
        anime.animate(s.labels, { opacity: 1, x: [-8, 0], duration: dur * 0.7, delay: anime.stagger(DEFAULTS.stagger, { start: dur * 0.3 }), ease: 'out(3)' });
      }
      el.dispatchEvent(new CustomEvent('toggle', { bubbles: true, detail: { collapsed: collapsed } }));
    }

    function setup(el) {
      var d = el.dataset;
      var a11y = !el.closest('[aria-hidden="true"]');
      el.classList.add('sbc');
      var toggle = el.querySelector(':scope > [data-toggle]');
      if (!toggle) {
        toggle = document.createElement(a11y ? 'button' : 'span');
        if (a11y) toggle.type = 'button';
        toggle.innerHTML = '<span class="sbc-chev">' + CHEVRON + '</span>Collapse';
        el.appendChild(toggle);
      }
      toggle.classList.add('sbc-toggle');
      var chev = toggle.querySelector('.sbc-chev') || toggle.querySelector('svg') || toggle;

      var items = [], labels = [];
      Array.prototype.slice.call(el.children).forEach(function (child) {
        var text = '';
        Array.prototype.slice.call(child.childNodes).forEach(function (n) {
          if (n.nodeType === 3 && n.textContent.trim()) {
            text += n.textContent.trim();
            var span = document.createElement('span');
            span.className = 'sbc-label';
            span.textContent = n.textContent;
            child.replaceChild(span, n);
            labels.push(span);
          } else if (n.nodeType === 1 && n.classList.contains('sbc-label')) {
            text += n.textContent.trim();
            labels.push(n);
          }
        });
        // Keep a name on icon-only items once the label has faded.
        if (a11y && text && /^(A|BUTTON)$/.test(child.tagName) && !child.getAttribute('aria-label') && child !== toggle) child.setAttribute('aria-label', text);
        items.push({ el: child, text: text });
      });

      var s = el._sbc = {
        toggle: toggle, chev: chev, items: items, labels: labels, a11y: a11y, collapsed: false,
        slim: +d.collapsed || DEFAULTS.collapsed,
        duration: +d.duration || DEFAULTS.duration
      };
      apply(el, d.start === 'collapsed', true);

      toggle.addEventListener('click', function () { apply(el, !s.collapsed, reduced()); });

      if (d.autoplay !== undefined && d.autoplay !== 'false' && !reduced()) {
        var timer = null, idleUntil = 0, canHover = window.matchMedia('(hover: hover)').matches;
        el.addEventListener('pointerdown', function () { idleUntil = Date.now() + 8000; });
        var tick = function () {
          if (Date.now() > idleUntil && !(canHover && el.matches(':hover')) && !el.contains(document.activeElement)) apply(el, !s.collapsed, false);
          timer = setTimeout(tick, 2200);
        };
        new IntersectionObserver(function (entries) {
          clearTimeout(timer);
          if (entries[0].isIntersecting) timer = setTimeout(tick, 900);
        }).observe(el);
      }
    }

    function init() {
      document.querySelectorAll('[data-sidebar-collapse]').forEach(function (el) {
        if (el._sbc) return;
        setup(el);
      });
    }

    window.animacious = window.animacious || {};
    window.animacious.sidebarCollapse = function (el) { if (el._sbc) apply(el, !el._sbc.collapsed, reduced()); };
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<aside data-sidebar-collapse>
  <a href="/"><svg>…</svg>Home</a>
  <a href="/search"><svg>…</svg>Search</a>
  <a href="/inbox"><svg>…</svg>Inbox</a>
</aside>