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
- Paste the snippet just before
</head>. - Add
data-folder-openand anaria-labelto a container whose children are the files, and give it a width. - Leave some room above it: that’s where the files fan out.
Options
Add any of these attributes to the same element.
data-color | Folder colour. Default #f5b83d. |
data-spread | Angle between neighbouring files in the fan. Default 12°. |
data-lift | How high the files rise, as a share of their height. Default 0.55. |
data-trigger | hover (also opens on tap) or click. Default hover. |
data-label | Text printed on the front of the folder. |
data-duration | Time to open. Default 700 ms. |
data-autoplay | Milliseconds: 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).
<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><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>