← Cards & images

Folder Open

A folder with its files tucked inside. Hover or tap it and the front flap swings down while the files rise out and fan open above it, each one tilting a little further. Move away and they slide back in.

Brief.pdf
Lake.jpg
Notes.txt
Budget.xls

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-folder-open and an aria-label to a container whose children are the files, and give it a width.
  3. Leave some room above it: that’s where the files fan out.

Options

Add any of these attributes to the same element.

data-colorFolder colour. Default #f5b83d.
data-spreadAngle between neighbouring files in the fan. Default 12°.
data-liftHow high the files rise, as a share of their height. Default 0.55.
data-triggerhover (also opens on tap) or click. Default hover.
data-labelText printed on the front of the folder.
data-durationTime to open. Default 700 ms.
data-autoplayMilliseconds: open and close on a timer until someone interacts.

The folder is drawn for you, with a tab, a back and a front flap, at a 4 : 3 shape unless you set an aspect-ratio. It’s a button with aria-expanded, so Enter and Space open it too. From code: animacious.folderOpen(element, true).

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Folder Open: a folder whose front flap swings down while the files
    // tucked inside rise out and fan open above it. Close it and they slide
    // back in. Opens on hover, tap or click, or on a timer.
    // Usage:
    //   <div data-folder-open aria-label="Project files">
    //     <div>Brief.pdf</div>
    //     <div>Logo.svg</div>
    //     <div>Photo.jpg</div>
    //   </div>
    // Each child is a file. Give the folder a width; its height follows
    // (aspect-ratio 4 / 3 unless you set one), and leave room above it for the fan.
    // Optional: data-color (folder colour, default #f5b83d), data-spread (fan
    // angle between files, degrees, default 12), data-lift (how high the files
    // rise, as a share of the folder's height, default 0.55), data-trigger
    // ("hover", default, also opens on tap; or "click"), data-autoplay (ms
    // between opening and closing, until someone interacts), data-duration
    // (ms, default 700).
    // Script use: animacious.folderOpen(el, true|false).
    var DEFAULTS = { color: '#f5b83d', spread: 12, lift: 0.55, duration: 700 };

    var css =
      '.fdo{position:relative;--fdo-c:#f5b83d;perspective:700px;-webkit-tap-highlight-color:transparent}' +
      ':where(.fdo){aspect-ratio:4/3}' +
      '.fdo-back{position:absolute;left:0;right:0;bottom:0;top:10%;border-radius:0 8% 8% 8%/0 10% 10% 10%;background:linear-gradient(rgba(0,0,0,.14),rgba(0,0,0,.2)),var(--fdo-c)}' +
      '.fdo-back::before{content:"";position:absolute;left:0;bottom:100%;width:40%;height:14%;min-height:8px;border-radius:6px 6px 0 0;background:inherit;clip-path:polygon(0 0,82% 0,100% 100%,0 100%)}' +
      '.fdo-files{position:absolute;left:13%;right:13%;top:10%;bottom:8%;display:block}' +
      '.fdo-files>*{position:absolute;left:0;right:0;bottom:0;height:100%;box-sizing:border-box;transform-origin:50% 100%}' +
      '.fdo-front{position:absolute;left:0;right:0;bottom:0;height:74%;border-radius:6%/8%;background:linear-gradient(rgba(255,255,255,.22),rgba(255,255,255,0) 40%,rgba(0,0,0,.06)),var(--fdo-c);transform-origin:50% 100%;box-shadow:0 -1px 0 rgba(255,255,255,.35) inset,0 8px 18px rgba(15,15,30,.18);display:flex;align-items:flex-end;justify-content:flex-start;padding:6% 8%;box-sizing:border-box;color:rgba(0,0,0,.55);font-weight:700}' +
      '.fdo-front:empty{padding:0}' +
      '.fdo[data-trigger="click"]{cursor:pointer}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

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

    function setup(el) {
      var d = el.dataset;
      var files = Array.prototype.slice.call(el.children);
      el.classList.add('fdo');
      el.style.setProperty('--fdo-c', d.color || DEFAULTS.color);
      var back = document.createElement('div');
      back.className = 'fdo-back';
      back.setAttribute('aria-hidden', 'true');
      var holder = document.createElement('div');
      holder.className = 'fdo-files';
      files.forEach(function (f) { holder.appendChild(f); });
      var front = document.createElement('div');
      front.className = 'fdo-front';
      front.setAttribute('aria-hidden', 'true');
      if (d.label) front.textContent = d.label;
      el.appendChild(back);
      el.appendChild(holder);
      el.appendChild(front);
      var s = el._fdoS = { files: files, front: front, open: false, timer: 0, anims: [] };
      rest(el);

      var touch = false;
      if (d.trigger !== 'click') {
        el.addEventListener('pointerenter', function (e) { if (e.pointerType === 'mouse') { stop(el); toggle(el, true); } });
        el.addEventListener('pointerleave', function (e) { if (e.pointerType === 'mouse') toggle(el, false); });
      }
      el.addEventListener('pointerdown', function (e) { touch = e.pointerType !== 'mouse'; });
      el.addEventListener('click', function () {
        if (d.trigger !== 'click' && !touch) return;
        stop(el);
        toggle(el, !s.open);
      });
      if (!el.hasAttribute('tabindex')) el.tabIndex = 0;
      el.setAttribute('role', 'button');
      el.setAttribute('aria-expanded', 'false');
      el.addEventListener('keydown', function (e) {
        if (e.key !== 'Enter' && e.key !== ' ') return;
        e.preventDefault();
        stop(el);
        toggle(el, !s.open);
      });
      if (el.closest('[aria-hidden="true"]')) { el.removeAttribute('tabindex'); el.removeAttribute('role'); el.removeAttribute('aria-expanded'); }

      if (d.autoplay !== undefined && d.autoplay !== 'false' && !reduced()) {
        var visible = false;
        new IntersectionObserver(function (en) {
          visible = en[0].isIntersecting;
          if (visible && !s.started) { s.started = true; setTimeout(function () { if (s.timer) toggle(el, true); }, 500); }
        }).observe(el);
        s.timer = setInterval(function () { if (visible) toggle(el, !s.open); }, +d.autoplay || 2400);
      }
    }

    // Closed: files tucked in a slightly ragged pile, front flap up.
    function rest(el) {
      var s = el._fdoS, n = s.files.length;
      s.files.forEach(function (f, i) {
        anime.utils.set(f, { x: 0, y: (n - 1 - i) * -3 + '%', rotate: (i % 2 ? 1 : -1) * 1.5, scale: 1 });
      });
      anime.utils.set(s.front, { rotateX: 0 });
    }

    function stop(el) {
      var s = el._fdoS;
      if (s && s.timer) { clearInterval(s.timer); s.timer = 0; }
    }

    function toggle(el, open) {
      var s = el._fdoS;
      if (!s || s.open === open) return;
      s.open = open;
      if (el.getAttribute('role')) el.setAttribute('aria-expanded', open);
      var d = el.dataset, n = s.files.length;
      var spread = d.spread !== undefined ? +d.spread : DEFAULTS.spread;
      var lift = (d.lift !== undefined ? +d.lift : DEFAULTS.lift) * 100;
      var dur = reduced() ? 0 : +d.duration || DEFAULTS.duration;
      s.anims.forEach(function (a) { a.pause(); });
      s.anims = [];
      s.anims.push(anime.animate(s.front, {
        rotateX: open ? -38 : 0,
        duration: dur,
        delay: open ? 0 : dur * 0.35,
        ease: open ? 'out(3)' : 'outBack(1.4)'
      }));
      s.files.forEach(function (f, i) {
        var k = i - (n - 1) / 2;
        var props = open
          ? { x: k * 26 + '%', y: (-lift + Math.abs(k) * 6) + '%', rotate: k * spread, scale: 1 }
          : { x: 0, y: (n - 1 - i) * -3 + '%', rotate: (i % 2 ? 1 : -1) * 1.5, scale: 1 };
        props.duration = dur;
        props.delay = open ? 90 + i * 60 : (n - 1 - i) * 40;
        props.ease = open ? 'outBack(1.3)' : 'inOut(3)';
        s.anims.push(anime.animate(f, props));
      });
    }

    function init() {
      document.querySelectorAll('[data-folder-open]').forEach(function (el) {
        if (el._fdo) return;
        el._fdo = true;
        setup(el);
      });
    }

    window.animacious = window.animacious || {};
    window.animacious.folderOpen = function (el, open) { stop(el); toggle(el, open === undefined ? !(el._fdoS && el._fdoS.open) : !!open); };
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-folder-open aria-label="Project files" style="width: 240px">
  <div class="file">Brief.pdf</div>
  <div class="file">Logo.svg</div>
  <div class="file">Photo.jpg</div>
</div>