Copied!
Navigation and lists

Menu rows to grid layout

A menu of big titles where hovering a row slides in an image strip and swaps the title style; clicking expands a cover and flies the row's images into a grid. Puzzle-piece art, no images.

← Library

Element · Vanilla JS + CSSUpdated November 6, 2026

Navigation and listsElementVanilla JS + CSS

Live preview needs JavaScript. The code below works without any dependencies.

Prompt for your AI assistant

Paste this into Claude, Cursor or ChatGPT. The settings update as you move the controls above.

Build "Menu rows to grid layout" for my site using the Enrichr Library element at https://enrichr.dev/elements/menu-rows-to-grid.html.

Read the machine-readable spec and source at https://enrichr.dev/elements/menu-rows-to-grid.json (markup, CSS, JavaScript, parameters).
Requirements: vanilla JavaScript and CSS, no new dependencies; keep the behaviour of the source unchanged; respect prefers-reduced-motion; work on touch screens; keep it accessible.

Settings:
- titles: Nobody's Love, Good Wings, Wild Honey, Slow Orbit, Paper Moon
- accent: #e6ff55
- cover: #005236
- duration: 0.9
- rows: 4

Verify: reduced motion shows no movement, it works at 375px wide, there are no console errors, and scrolling stays smooth.

The same spec as JSON, for tools that read it directly: /elements/menu-rows-to-grid.json.

Code

Mount it on an element that contains the markup. It returns an object with update(options) and destroy().

HTML

<div class="ef-mtg">
  <svg class="ef-mtg__defs" width="0" height="0" aria-hidden="true" focusable="false"><defs>
<symbol id="efmtg-p1" viewBox="1513.7 1453.9 322.3 322.3">
    <path fill="currentColor" d="M1821 1761.2h-292.3v-111.6c0-8 6.5-14.5 14.5-14.5 4.2 0 8.2 1.9 11 5.1 7.8 8.6 19.7 13.6 32.7 11.9 16.8-2.3 30.1-16 32-32.7 2.5-22.6-15.1-41.6-37.1-41.6-11 0-20.9 4.7-27.7 12.3-2.8 3.1-6.7 4.9-10.9 4.9-8 0-14.5-6.4-14.5-14.4v-111.7h292.3v111.7c0 8-6.4 14.4-14.5 14.4-4.2 0-8.1-1.8-10.9-5-7.9-8.6-19.7-13.6-32.7-11.9-16.8 2.2-30.1 15.9-32 32.7-2.5 22.6 15.1 41.6 37.1 41.6 11 0 20.8-4.8 27.7-12.3 2.8-3.1 6.7-5 10.8-5 8.1 0 14.5 6.5 14.5 14.5z"/>
  </symbol>
<symbol id="efmtg-p4" viewBox="1423.36 860.8 503.07 322.3">
    <path fill="currentColor" d="M1821 875.8v111.6c0 8 6.5 14.5 14.5 14.5 4.2 0 8.1-1.9 10.9-5 6.8-7.5 16.7-12.3 27.7-12.3 22 0 39.6 19 37.1 41.6-1.9 16.7-15.2 30.5-32 32.7-13 1.7-24.8-3.3-32.7-11.9-2.8-3.2-6.8-5-11-5-8 0-14.5 6.4-14.5 14.4v111.7h-111.6c-8 0-14.5-6.5-14.5-14.5 0-4.2 1.9-8.2 5-11 8.7-7.9 13.7-19.7 12-32.7-2.3-16.8-16-30.1-32.8-32-22.5-2.5-41.5 15.1-41.5 37.1 0 11 4.7 20.9 12.3 27.7 3.1 2.8 4.9 6.7 4.9 10.8v0.1c0 8-6.4 14.5-14.5 14.5h-111.6v-111.7c0-8-6.4-14.4-14.5-14.4-4.1 0-8 1.8-10.8 4.9-6.9 7.6-16.7 12.3-27.7 12.3-22 0-39.6-19-37.1-41.6 1.9-16.7 15.2-30.4 32-32.7 13-1.7 24.8 3.3 32.7 11.9 2.8 3.2 6.7 5.1 10.9 5.1 8.1 0 14.5-6.5 14.5-14.5v-111.6z"/>
  </symbol>
<symbol id="efmtg-p8" viewBox="1513.7 166.86 322.3 503.06">
    <path fill="currentColor" d="M1528.7 564.5v-111.6c0-8 6.5-14.5 14.5-14.5 4.2 0 8.2 1.9 11 5 7.8 8.7 19.7 13.7 32.7 11.9 16.8-2.2 30.1-15.9 32-32.7 2.5-22.5-15.1-41.5-37.1-41.5-11 0-20.9 4.7-27.7 12.3-2.8 3.1-6.7 4.9-10.9 4.9-8 0-14.5-6.5-14.5-14.5v-111.6h111.6c8.1 0 14.5-6.5 14.5-14.5 0-4.1-1.8-8.1-4.9-10.9-7.6-6.8-12.3-16.6-12.3-27.6 0-22 19-39.6 41.5-37.1 16.8 1.8 30.5 15.2 32.8 32 1.7 12.9-3.3 24.8-12 32.7-3.1 2.8-5 6.7-5 10.9 0 8 6.5 14.5 14.5 14.5h111.6v111.6c0 8-6.4 14.5-14.5 14.5-4.2 0-8.1-1.9-10.9-5-7.9-8.7-19.7-13.6-32.7-11.9-16.8 2.2-30.1 15.9-32 32.7-2.5 22.5 15.1 41.6 37.1 41.6 11 0 20.8-4.8 27.7-12.3 2.8-3.1 6.7-5 10.8-5 8.1 0 14.5 6.5 14.5 14.5v111.6h-111.6c-8 0-14.5 6.5-14.5 14.5 0 4.2 1.9 8.1 5 10.9 7.5 6.8 12.3 16.7 12.3 27.7 0 22-19 39.5-41.6 37.1-16.8-1.9-30.5-15.3-32.7-32-1.7-13 3.3-24.9 11.9-32.7 3.1-2.9 5-6.8 5-11 0-8-6.4-14.5-14.5-14.5z"/>
  </symbol>
  </defs></svg>
  <div class="ef-mtg__stage"></div>
</div>

CSS

/* Enrichr Library · https://enrichr.dev */
/* Adapted from https://github.com/codrops/MenuToGrid (MIT) — Copyright (c) 2009 - 2021 Codrops */
.ef-mtg { width: 100%; }
.ef-mtg__defs { position: absolute; width: 0; height: 0; overflow: hidden; }
.ef-mtg__stage { --acc: #e6ff55; --cov: #005236; position: relative; box-sizing: border-box; width: 100%; height: 24rem; overflow: hidden; border-radius: 1rem; background: #0c1e18; color: #fbf8f1; font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
.ef-mtg__rows { position: absolute; inset: 0; display: flex; flex-direction: column; }
.ef-mtg__row { flex: 1 1 0; min-height: 0; display: flex; align-items: center; justify-content: space-between; gap: 1rem; box-sizing: border-box; width: 100%; padding: 0 1.2rem; margin: 0; background: none; border: 0; border-top: 1px solid rgba(251, 248, 241, 0.18); color: inherit; font: inherit; text-align: left; cursor: pointer; }
.ef-mtg__row:last-child { border-bottom: 1px solid rgba(251, 248, 241, 0.18); }
.ef-mtg__row:focus-visible { outline: 2px solid var(--acc); outline-offset: -4px; }
.ef-mtg__t { display: block; overflow: hidden; padding: 0.15em 0; }
.ef-mtg__t > span { display: block; font-size: 1.65rem; font-weight: 800; letter-spacing: -0.03em; line-height: 1.05; text-transform: uppercase; transform-origin: 0 50%; will-change: transform; }
.ef-mtg__t.is-switch > span { color: var(--acc); font-style: italic; letter-spacing: -0.045em; }
.ef-mtg__imgs { display: flex; gap: 0.45rem; height: 62%; max-height: 4.2rem; flex: 0 0 auto; }
.ef-mtg__img { position: relative; overflow: hidden; border-radius: 0.4rem; flex: 0 0 auto; transform-origin: 0 0; }
.ef-mtg__imgs .ef-mtg__img { width: 3.1rem; height: 100%; opacity: 0; transform: translateX(20%) scale(0.8); transition: opacity 400ms cubic-bezier(.2,.8,.2,1), transform 400ms cubic-bezier(.2,.8,.2,1); }
.ef-mtg__imgs .ef-mtg__img:nth-child(1) { transition-delay: 105ms; }
.ef-mtg__imgs .ef-mtg__img:nth-child(2) { transition-delay: 70ms; }
.ef-mtg__imgs .ef-mtg__img:nth-child(3) { transition-delay: 35ms; }
.ef-mtg__row.is-hover .ef-mtg__imgs .ef-mtg__img { opacity: 1; transform: none; }
.ef-mtg__row.is-snap .ef-mtg__imgs .ef-mtg__img { transition: none; transition-delay: 0s; opacity: 1; transform: none; }
.ef-mtg__piece { position: absolute; left: 50%; top: 50%; width: 64%; height: 64%; transform: translate(-50%, -50%); display: block; opacity: 0.92; }
.ef-mtg__cover { position: absolute; inset: 0; z-index: 2; background: var(--cov); opacity: 0; pointer-events: none; clip-path: inset(0 0 100% 0); }
.ef-mtg__prev { position: absolute; inset: 0; z-index: 3; display: none; flex-direction: column; box-sizing: border-box; padding: 1rem 1.2rem 1.2rem; }
.ef-mtg__prev.is-current { display: flex; }
.ef-mtg__pt { margin: 0; overflow: hidden; padding: 0.1em 0; flex: 0 0 auto; }
.ef-mtg__pt > span { display: block; font-size: 1.9rem; font-weight: 800; letter-spacing: -0.04em; line-height: 1.05; text-transform: uppercase; color: var(--acc); font-style: italic; transform-origin: 100% 50%; will-change: transform; }
.ef-mtg__grid { flex: 1 1 0; min-height: 0; margin-top: 0.8rem; display: grid; grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(2, 1fr); gap: 0.55rem; }
.ef-mtg__grid .ef-mtg__img { width: 100%; height: 100%; border-radius: 0.5rem; }
.ef-mtg__close { position: absolute; z-index: 4; top: 1.05rem; right: 1.2rem; display: none; padding: 0.45rem 0.8rem; border-radius: 99px; border: 1.5px solid rgba(251, 248, 241, 0.7); background: none; color: #fbf8f1; font: inherit; font-size: 0.68rem; font-weight: 800; letter-spacing: 0.14em; text-transform: uppercase; cursor: pointer; opacity: 0; transition: background 250ms, color 250ms; }
.ef-mtg__close.is-show { display: block; }
.ef-mtg__close:hover { background: var(--acc); color: #0c1e18; border-color: var(--acc); }
.ef-mtg__close:focus-visible { outline: 2px solid var(--acc); outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .ef-mtg__imgs .ef-mtg__img { transition: opacity 200ms; transform: none; } .ef-mtg__close { transition: none; } }

JavaScript

// Enrichr Library · https://enrichr.dev
// Adapted from https://github.com/codrops/MenuToGrid (MIT) — Copyright (c) 2009 - 2021 Codrops
// Menu rows to grid: hovering a row slides a strip of images in and swaps its title; clicking expands a cover over the whole box and the row's
// images fly (hand-rolled FLIP, measured first and last rectangles) into a grid where more tiles pop in. Close reverses it.
// Vanilla port, no GSAP or Flip plugin: everything uses the Web Animations API. Rows are real buttons, Escape closes, reduced motion skips the movement.
// mount(root, { titles (comma list), accent, cover, duration (s), rows (2-5) })
function mount(root, o) {
  var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  var stage = root.querySelector(".ef-mtg__stage");
  var opts = {}, rows = [], anims = [], offs = [], timers = [];
  var cur = -1, busy = false, isOpen = false;
  var EASE = "cubic-bezier(0.83, 0, 0.17, 1)", EXPO = "cubic-bezier(0.16, 1, 0.3, 1)";
  var GRADS = [
    "linear-gradient(160deg, #0a6b46, #0c1e18)", "linear-gradient(200deg, #c6f26b, #2f7d5b)", "radial-gradient(circle at 30% 30%, #e6ff55, #005236 70%)", "linear-gradient(135deg, #0c1e18, #2f7d5b)",
    "linear-gradient(180deg, #fbf8f1, #c6f26b)", "conic-gradient(from 200deg at 40% 60%, #005236, #c6f26b, #0a6b46)", "linear-gradient(150deg, #2f7d5b, #005236)", "radial-gradient(circle at 70% 70%, #c6f26b, #0c1e18 75%)"
  ];
  var PIECES = ["p1", "p4", "p8"];
  var cover, closeBtn, rowsWrap;

  function el(tag, cls, txt) { var e = document.createElement(tag); if (cls) e.className = cls; if (txt != null) e.textContent = txt; return e; }
  function ms(f) { return reduce ? 1 : Math.max(1, opts.duration * 1000 * f); }
  function play(node, kf, o2) {
    var a = node.animate(kf, Object.assign({ fill: "both" }, o2));
    anims.push(a);
    return a;
  }
  function flush() { anims.forEach(function (a) { try { a.cancel(); } catch (e) {} }); anims = []; }
  function later(fn, t) { var id = setTimeout(fn, t); timers.push(id); return id; }
  function clearTimers() { timers.forEach(clearTimeout); timers = []; }
  function art(i, withPiece) {
    var d = el("div", "ef-mtg__img");
    d.style.background = GRADS[i % GRADS.length];
    if (withPiece) {
      var s = document.createElementNS("http://www.w3.org/2000/svg", "svg");
      s.setAttribute("class", "ef-mtg__piece"); s.setAttribute("aria-hidden", "true"); s.setAttribute("focusable", "false");
      var u = document.createElementNS("http://www.w3.org/2000/svg", "use");
      u.setAttribute("href", "#efmtg-" + PIECES[i % 3]);
      s.appendChild(u);
      s.style.color = i % 3 === 0 ? opts.accent : i % 3 === 1 ? "#fbf8f1" : "#c6f26b";
      d.appendChild(s);
    }
    return d;
  }
  function on(t, ev, fn) { t.addEventListener(ev, fn); offs.push(function () { t.removeEventListener(ev, fn); }); }

  function teardown() {
    flush(); clearTimers();
    offs.forEach(function (f) { f(); }); offs = [];
    stage.textContent = ""; rows = []; cur = -1; busy = false; isOpen = false;
  }

  function build() {
    teardown();
    stage.style.setProperty("--acc", opts.accent);
    stage.style.setProperty("--cov", opts.cover);
    var names = String(opts.titles || "").split(",").map(function (s) { return s.trim(); }).filter(Boolean);
    if (!names.length) names = ["Untitled"];
    rowsWrap = el("div", "ef-mtg__rows");
    cover = el("div", "ef-mtg__cover");
    closeBtn = el("button", "ef-mtg__close", "Close");
    closeBtn.type = "button"; closeBtn.setAttribute("aria-label", "Close and return to the menu");
    stage.appendChild(rowsWrap); stage.appendChild(cover);
    for (var r = 0; r < opts.rows; r++) (function (r) {
      var btn = el("button", "ef-mtg__row"); btn.type = "button";
      var tw = el("span", "ef-mtg__t"), t = el("span", "", names[r % names.length]); tw.appendChild(t);
      var imgs = el("span", "ef-mtg__imgs"); imgs.setAttribute("aria-hidden", "true");
      var list = [];
      for (var k = 0; k < 4; k++) { var im = art(r * 4 + k, k % 2 === 0); imgs.appendChild(im); list.push(im); }
      btn.appendChild(tw); btn.appendChild(imgs);
      btn.setAttribute("aria-label", "Open " + names[r % names.length]);
      rowsWrap.appendChild(btn);
      var prev = el("div", "ef-mtg__prev"); prev.setAttribute("role", "region"); prev.setAttribute("aria-label", names[r % names.length]);
      var pt = el("h3", "ef-mtg__pt"), pts = el("span", "", names[r % names.length]); pt.appendChild(pts);
      var grid = el("div", "ef-mtg__grid"), extras = [];
      for (var j = 0; j < 4; j++) { var ex = art(r * 4 + 4 + j, j % 2 === 1); ex.style.opacity = "1"; grid.appendChild(ex); extras.push(ex); }
      prev.appendChild(pt); prev.appendChild(grid);
      stage.appendChild(prev);
      var row = { btn: btn, tw: tw, t: t, imgs: imgs, list: list, prev: prev, pt: pts, grid: grid, extras: extras };
      rows.push(row);
      function enter() { if (isOpen || busy) return; hover(row, true); }
      function leave() { if (isOpen || busy) return; hover(row, false); }
      on(btn, "pointerenter", enter); on(btn, "pointerleave", leave);
      on(btn, "focus", enter); on(btn, "blur", leave);
      on(btn, "click", function () { open(r); });
    })(r);
    stage.appendChild(closeBtn);
    on(closeBtn, "click", close);
    on(stage, "keydown", function (e) { if (e.key === "Escape" && isOpen) close(); });
  }

  function hover(row, state) {
    if (row.hov === state) return;
    row.hov = state;
    row.btn.classList.toggle("is-hover", state);
    if (reduce) { row.tw.classList.toggle("is-switch", state); return; }
    var a = play(row.t, [{ transform: "translateY(0)" }, { transform: "translateY(-100%)" }], { duration: 100, easing: "ease-in" });
    a.onfinish = function () {
      row.tw.classList.toggle("is-switch", state);
      play(row.t, [{ transform: "translateY(100%) rotate(15deg)" }, { transform: "translateY(0) rotate(0)" }], { duration: 500, easing: EXPO });
    };
  }

  function open(i) {
    if (busy || isOpen) return;
    busy = true; isOpen = true; cur = i;
    var row = rows[i], H = stage.clientHeight;
    var top = row.btn.offsetTop, h = row.btn.offsetHeight;
    row.hov = true; row.btn.classList.add("is-hover", "is-snap");
    row.tw.classList.add("is-switch");
    var D = ms(1);
    rows.forEach(function (rw) { rw.btn.tabIndex = -1; rw.btn.setAttribute("aria-hidden", "true"); });
    // cover grows from the row to the full box
    cover.style.opacity = "1";
    play(cover, [{ clipPath: "inset(" + top + "px 0 " + (H - top - h) + "px 0)" }, { clipPath: "inset(0 0 0 0)" }], { duration: D, easing: EASE });
    // every title slides out, away from the clicked row
    rows.forEach(function (rw, n) {
      play(rw.t, [{ transform: "translateY(0)" }, { transform: "translateY(" + (n > i ? 110 : -110) + "%)" }], { duration: ms(0.55), easing: EASE });
    });
    // FLIP the row's images into the preview grid
    var first = row.list.map(function (im) { return im.getBoundingClientRect(); });
    row.prev.classList.add("is-current");
    for (var k = row.list.length - 1; k >= 0; k--) row.grid.insertBefore(row.list[k], row.grid.firstChild);
    row.list.forEach(function (im, k2) {
      var l = im.getBoundingClientRect(), f = first[k2];
      play(im, [
        { transformOrigin: "0 0", transform: "translate(" + (f.left - l.left) + "px," + (f.top - l.top) + "px) scale(" + (f.width / l.width) + "," + (f.height / l.height) + ")" },
        { transformOrigin: "0 0", transform: "none" }
      ], { duration: D, easing: EASE, delay: reduce ? 0 : k2 * 40 });
    });
    row.extras.forEach(function (ex, j) {
      play(ex, [{ opacity: 0, transform: "scale(0) translateY(" + Math.round(Math.random() * 120) + "%)" }, { opacity: 1, transform: "none" }], { duration: D, easing: EASE, delay: reduce ? 0 : (4 + j) * 40 });
    });
    // preview title and close button
    play(row.pt, [{ transform: "translateY(-100%) rotate(15deg)" }, { transform: "translateY(0) rotate(0)" }], { duration: D, easing: EASE });
    closeBtn.classList.add("is-show");
    play(closeBtn, [{ opacity: 0 }, { opacity: 1 }], { duration: D, easing: EASE });
    later(function () { busy = false; closeBtn.focus({ preventScroll: true }); }, D + 200);
  }

  function close() {
    if (busy || !isOpen) return;
    busy = true;
    var row = rows[cur], H = stage.clientHeight;
    var top = row.btn.offsetTop, h = row.btn.offsetHeight, D = ms(0.55);
    var all = row.list.concat(row.extras);
    all.forEach(function (im, n) {
      play(im, [{ opacity: 1, transform: "none" }, { opacity: 0, transform: "scale(0)" }], { duration: D, easing: EASE, delay: reduce ? 0 : n * 40 });
    });
    play(row.pt, [{ transform: "translateY(0)" }, { transform: "translateY(100%)" }], { duration: ms(0.65), easing: EASE });
    play(closeBtn, [{ opacity: 1 }, { opacity: 0 }], { duration: D, easing: EASE });
    var cd = reduce ? 0 : D * 0.8;
    play(cover, [{ clipPath: "inset(0 0 0 0)", opacity: 1 }, { clipPath: "inset(" + (top + h / 2) + "px 0 " + (H - top - h / 2) + "px 0)", opacity: 1 }], { duration: ms(0.55), easing: EXPO, delay: cd });
    later(function () {
      // titles slide back, staggered outward from the closed row
      rows.forEach(function (rw, n) {
        var from = n > cur ? 110 : -110;
        play(rw.t, [{ transform: "translateY(" + from + "%)" }, { transform: "translateY(0)" }], { duration: ms(0.5), easing: EASE, delay: reduce ? 0 : Math.abs(n - cur) * 30 });
      });
    }, cd + ms(0.3));
    later(function () {
      row.list.forEach(function (im) { row.imgs.appendChild(im); });
      flush();
      cover.style.opacity = "0";
      row.prev.classList.remove("is-current");
      closeBtn.classList.remove("is-show");
      row.btn.classList.remove("is-hover", "is-snap");
      row.tw.classList.remove("is-switch");
      row.hov = false;
      rows.forEach(function (rw) { rw.btn.tabIndex = 0; rw.btn.removeAttribute("aria-hidden"); });
      isOpen = false; busy = false;
      row.btn.focus({ preventScroll: true });
    }, cd + ms(0.55) + ms(0.3) + 60);
  }

  function update(next) {
    var prev = opts;
    opts = Object.assign({ titles: "Nobody's Love, Good Wings, Wild Honey, Slow Orbit, Paper Moon", accent: "#e6ff55", cover: "#005236", duration: 0.9, rows: 4 }, opts, next || {});
    opts.rows = Math.max(2, Math.min(5, Math.round(+opts.rows || 4)));
    opts.duration = Math.max(0.2, Math.min(3, +opts.duration || 0.9));
    if (prev.titles !== opts.titles || prev.accent !== opts.accent || prev.cover !== opts.cover || prev.rows !== opts.rows) build();
  }
  update(o);
  return { update: update, destroy: teardown };
}

// Usage
var fx = mount(document.querySelector("#my-root"), {"titles":"Nobody's Love, Good Wings, Wild Honey, Slow Orbit, Paper Moon","accent":"#e6ff55","cover":"#005236","duration":0.9,"rows":4});

How it works

This is a vanilla port with no GSAP or Flip plugin; everything uses the Web Animations API. Hover reveals each row's four images with staggered CSS transitions and swaps the title with a short slide out and a rotated slide in. On click, a cover element clips open from the clicked row's rectangle to the whole box, the titles slide away, and the row's images are moved into a preview grid. Their first and last rectangles are measured, and each image animates from the inverse transform to none (a hand-written FLIP) while the remaining grid tiles pop in. Close reverses the sequence and puts the images back in their row.

Accessibility and performance

Rows are real buttons with labels, so the menu works from the keyboard and focus shows the same hover preview. Focus moves to Close when a row opens and back to the row afterwards, and Escape closes. Hidden rows are removed from the tab order while the grid is open. With reduced motion the image strip fades and everything else changes state with no travel.

The cover animates clip-path and the images animate transform and opacity. Nothing runs when idle. Animations are cancelled on close and on destroy, so no timers or listeners remain.

SectionNavigation and lists
TechniquesReveal and intro, Stagger
StackVanilla JS, CSS
Works withReact, Next.js, Vanilla
DependenciesNone
Statustested
On CodePenOpen the live pen on our CodePen account; fork it to experiment.
Inspired byMenu to Grid Layout Animation by Codrops on github.com. Original code, written from scratch.

Find this technique on a real site

Want to see this technique in the wild? Check any site with the free tools, then build your own version from this page.

Check which animation library a site uses · Extract a site's design tokens · Browse site breakdowns

Licence and provenance

Free to use in personal and client projects during the preview. Final licence terms will be published here before any paid tier starts; ask before reselling the code as a collection.

This is original code. The technique is a common one that we studied by measuring award-winning sites with Enrichr; no third-party code, shaders, fonts or images are used.

Rapid layers introA stack of full-bleed colour layers wipes in quickly one after another, then wipes away to reveal a headline, with a replay button. Island menuA round menu button that stretches into a wide pill, spins its logo in, turns its bars into a cross and pops a panel of links out of the corner. Staggered grid revealA grid of tiles that flip up one after another in random, reading, centre-out or edges-in order, then spin their glyphs into place.

FAQ

Can the grid hold real photos?

Yes. Give each .ef-mtg__img a background-image in art(). The measuring and FLIP code does not care what is inside.

How many images does each row have?

Four in the row plus four extras that pop into the grid, giving a 4 by 2 layout. Change the loops in build() and the grid-template in the CSS to alter it.

Does the cover need to be a solid colour?

No. It is a single element with a clip-path, so a gradient or image background works as well.