← Navigation

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

  1. Paste the snippet just before </head>.
  2. Add data-card-nav to your <nav>. Put your logo first, then anything for the right-hand end (a sign-up button, say), then a <div data-cards>.
  3. Each child of data-cards is a card: a title, then its links. The script adds the menu button and turns the rest into the bar.
  4. 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-colorsCard colours, comma-separated, used in turn. Default #2e1065,#6a0dad,#a855f7.
data-durationHow long it takes to spring open. Default 650 ms.
data-autoplayIt 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).

1 · Snippet
<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>
2 · Your HTML
<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>