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
- Paste the snippet just before
</head>. - Add
data-sidebar-collapseto your sidebar. Each child is an item: an icon (ansvg,imgori) followed by its text. - 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 childrendata-toggleto use that instead.
Options
Add any of these attributes to the same element.
data-collapsed | Width when collapsed, in px: the icon plus the padding round it. Default 64. |
data-start | Set to collapsed to start slim. |
data-duration | How long it takes to fold. Default 450 ms. |
data-autoplay | It 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.
<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><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>