Card Nav
A slim floating navbar with a menu button. Tap it and the bar springs open downwards with a little overshoot, and a row of coloured link cards rises into it one after another. Tap again and the cards drop away as the bar folds back up. On a phone the cards stack into rows.
How to use it
- Paste the snippet just before
</head>. - Add
data-card-navto your<nav>. Put your logo first, then anything for the right-hand end (a sign-up button, say), then a<div data-cards>. - Each child of
data-cardsis a card: a title, then its links. The script adds the menu button and turns the rest into the bar. - Position the nav yourself (for example
position: absolute; top: 16px; left: 16px; right: 16px) so it opens over the page, and give it a background.
Options
Add any of these attributes to the same element.
data-colors | Card colours, comma-separated, used in turn. Default #2e1065,#6a0dad,#a855f7. |
data-duration | How long it takes to spring open. Default 650 ms. |
data-autoplay | It opens and closes itself while in view, pausing when someone taps. Handy for demos. |
The menu button gets aria-expanded and the closed cards are out of the tab order. Escape or picking a link closes it, and it fires a toggle event (e.detail.open). From code, animacious.cardNav(nav) opens or closes it. Set the bar height with the CSS variable --cnv-bar (default 56px).
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Card Nav: a slim navbar with a menu button. Tap it and the bar springs
// open downwards into a panel holding a row of link cards, which rise
// into place one after another. Tap again and the cards drop away as the
// bar folds back up.
// Usage: <nav data-card-nav>
// <a href="/">Logo</a>
// <a href="/signup" data-cta>Get started</a>
// <div data-cards>
// <div><b>About</b><a href="/company">Company</a><a href="/careers">Careers</a></div>
// <div><b>Projects</b><a href="/featured">Featured</a>…</div>
// </div>
// </nav>
// The children of [data-cards] are the cards: a title, then links. The
// first other child (your logo) sits in the middle of the bar and the rest
// on the right. The script adds the menu button on the left.
// Position the nav yourself (e.g. position: absolute; top: 16px; left: 16px;
// right: 16px) so it opens over the page.
// Optional: data-colors (comma-separated card colours, default
// #2e1065,#6a0dad,#a855f7), data-duration (ms, default 650), data-autoplay
// (it opens and closes itself while in view; handy for demos).
// Escape or picking a link closes it. Listen for the "toggle" event:
// e.detail.open is true or false.
// Script use: animacious.cardNav(nav) opens or closes it.
var DEFAULTS = { colors: '#2e1065,#6a0dad,#a855f7', duration: 650, stagger: 70 };
var uid = 0;
var css =
'.cnv{display:block;overflow:hidden;box-sizing:border-box}' +
'.cnv-bar{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:8px;height:var(--cnv-bar,56px);padding:0 10px;box-sizing:border-box}' +
'.cnv-mid{justify-self:center;display:flex;align-items:center}' +
'.cnv-end{justify-self:end;display:flex;align-items:center;gap:8px}' +
'.cnv-btn{position:relative;justify-self:start;width:40px;height:40px;padding:0;border:0;border-radius:10px;background:none;color:inherit;cursor:pointer;-webkit-tap-highlight-color:transparent}' +
'.cnv-line{position:absolute;left:11px;right:11px;top:50%;height:2px;margin-top:-1px;border-radius:2px;background:currentColor;pointer-events:none}' +
'.cnv-body{padding:0 8px 8px;visibility:hidden}' +
'.cnv-show .cnv-body{visibility:visible}' +
'.cnv-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px}' +
'.cnv-card{display:flex;flex-direction:column;gap:2px;min-width:0;padding:12px 14px;border-radius:12px;box-sizing:border-box;color:#fff}' +
'.cnv-card>:first-child{font-size:1.2em;font-weight:600;letter-spacing:-.02em;margin-bottom:auto;padding-bottom:10px}' +
'.cnv-card a{color:inherit;text-decoration:none;opacity:.9}' +
'.cnv-card a::before{content:"\\2197";display:inline-block;margin-right:.35em;font-size:.85em}' +
'.cnv-card a:hover{opacity:1;text-decoration:underline}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function burger(s, open, still) {
var to = open ? [{ y: 0, rotate: 45 }, { y: 0, rotate: -45 }] : [{ y: -4, rotate: 0 }, { y: 4, rotate: 0 }];
s.lines.forEach(function (l, k) {
if (still) anime.utils.set(l, to[k]);
else anime.animate(l, Object.assign({ duration: 380, ease: 'inOut(3)' }, to[k]));
});
}
function toggle(el) {
var s = el._cnv;
var open = (s.open = !s.open);
var barH = s.bar.offsetHeight;
el.classList.toggle('cnv-open', open);
if (s.a11y) {
s.btn.setAttribute('aria-expanded', open);
s.btn.setAttribute('aria-label', open ? 'Close menu' : 'Menu');
if (open) s.body.removeAttribute('inert'); else s.body.setAttribute('inert', '');
}
el.dispatchEvent(new CustomEvent('toggle', { bubbles: true, detail: { open: open } }));
anime.utils.remove([el].concat(s.cards));
var still = reduced();
burger(s, open, still);
var from = el.offsetHeight;
var full = barH + s.body.offsetHeight;
var D = s.duration;
if (open) {
el.classList.add('cnv-show');
if (still) { el.style.height = ''; anime.utils.set(s.cards, { opacity: 1, y: 0 }); return; }
anime.animate(el, {
height: [from, full], duration: D, ease: 'outBack(1.1)',
onComplete: function () { if (s.open) el.style.height = ''; }
});
anime.animate(s.cards, {
opacity: [0, 1], y: [48, 0], scale: [0.96, 1],
duration: D, delay: anime.stagger(s.stagger, { start: 90 }), ease: 'outBack(1.4)'
});
} else {
var hide = function () { if (!s.open) el.classList.remove('cnv-show'); };
if (still) { el.style.height = barH + 'px'; anime.utils.set(s.cards, { opacity: 0 }); hide(); return; }
anime.animate(s.cards, { opacity: 0, y: 24, duration: 220, delay: anime.stagger(30, { from: 'last' }), ease: 'in(2)' });
anime.animate(el, { height: [from, barH], duration: D * 0.7, delay: 120, ease: 'inOut(3)', onComplete: hide });
}
}
function setup(el) {
var d = el.dataset;
var box = el.querySelector('[data-cards]');
if (!box) return;
var a11y = !el.closest('[aria-hidden="true"]');
var rest = Array.prototype.filter.call(el.children, function (c) { return c !== box; });
el.classList.add('cnv');
var bar = document.createElement('div');
bar.className = 'cnv-bar';
var btn = document.createElement(a11y ? 'button' : 'span');
btn.className = 'cnv-btn';
var lines = [0, 1].map(function () {
var l = document.createElement('span');
l.className = 'cnv-line';
l.setAttribute('aria-hidden', 'true');
btn.appendChild(l);
return l;
});
var mid = document.createElement('div');
mid.className = 'cnv-mid';
var end = document.createElement('div');
end.className = 'cnv-end';
rest.forEach(function (c, k) { (k === 0 ? mid : end).appendChild(c); });
bar.appendChild(btn);
bar.appendChild(mid);
bar.appendChild(end);
var body = document.createElement('div');
body.className = 'cnv-body';
body.id = 'card-nav-' + (++uid);
box.classList.add('cnv-cards');
body.appendChild(box);
el.insertBefore(bar, el.firstChild);
el.appendChild(body);
var colors = (d.colors || DEFAULTS.colors).split(',').map(function (c) { return c.trim(); });
var cards = Array.prototype.slice.call(box.children);
cards.forEach(function (c, k) {
c.classList.add('cnv-card');
if (!c.style.background && colors.length) c.style.background = colors[k % colors.length];
});
if (a11y) {
btn.type = 'button';
btn.setAttribute('aria-label', 'Menu');
btn.setAttribute('aria-expanded', 'false');
btn.setAttribute('aria-controls', body.id);
body.setAttribute('inert', '');
}
var s = el._cnv = {
bar: bar, btn: btn, lines: lines, body: body, cards: cards, open: false, a11y: a11y,
duration: parseFloat(d.duration) || DEFAULTS.duration, stagger: DEFAULTS.stagger
};
el.style.height = bar.offsetHeight + 'px';
anime.utils.set(cards, { opacity: 0 });
burger(s, false, true);
btn.addEventListener('click', function () { toggle(el); });
body.addEventListener('click', function (e) {
var a = e.target.closest('a');
if (!a || !s.open) return;
if (a.getAttribute('href') === '#') e.preventDefault();
toggle(el);
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && s.open) { toggle(el); if (a11y) btn.focus(); }
});
// Closed, keep the nav as tall as its bar if the bar changes size.
window.addEventListener('resize', function () { if (!s.open) el.style.height = bar.offsetHeight + 'px'; });
if (d.autoplay !== undefined && d.autoplay !== 'false' && !reduced()) {
var timer = null, idleUntil = 0;
el.addEventListener('pointerdown', function () { idleUntil = Date.now() + 8000; });
var tick = function () {
if (Date.now() > idleUntil && !el.contains(document.activeElement)) toggle(el);
timer = setTimeout(tick, s.open ? 2600 : 1400);
};
new IntersectionObserver(function (entries) {
clearTimeout(timer);
if (entries[0].isIntersecting) timer = setTimeout(tick, 700);
}).observe(el);
}
}
function init() {
document.querySelectorAll('[data-card-nav]').forEach(function (el) {
if (el._cnv) return;
setup(el);
});
}
window.animacious = window.animacious || {};
window.animacious.cardNav = function (el) { if (el._cnv) toggle(el); };
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><nav data-card-nav class="site-nav">
<a href="/">Logo</a>
<a href="/signup">Get started</a>
<div data-cards>
<div><b>About</b><a href="/company">Company</a><a href="/careers">Careers</a></div>
<div><b>Projects</b><a href="/featured">Featured</a><a href="/cases">Case studies</a></div>
<div><b>Contact</b><a href="/email">Email</a><a href="/linkedin">LinkedIn</a></div>
</div>
</nav>