Copied!
Gallery

Glitchy grid layout slideshow

A grid of puzzle-piece tiles that drift against the pointer. Switching layouts sends the tiles out, then stacks the next set in the centre and settles them into the grid, and a magnetic button fires a clipped glitch.

← Library

Element · Vanilla JS + CSS + Web Animations APIUpdated November 6, 2026

CursorElementVanilla JS + CSS + Web Animations API

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 "Glitchy grid layout slideshow" for my site using the Enrichr Library element at https://enrichr.dev/elements/glitchy-grid-stack.html.

Read the machine-readable spec and source at https://enrichr.dev/elements/glitchy-grid-stack.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:
- accent: #e6ff55
- parallax: 20
- duration: 1
- glitchGap: 10
- glitchTime: 2

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/glitchy-grid-stack.json.

Code

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

HTML

<div class="ef-ggs">
  <div class="ef-ggs__wrap" role="group" aria-roledescription="slideshow" aria-label="Puzzle grid layouts">
    <div class="ef-ggs__stage">
      <div class="ef-ggs__slides"></div>
      <button type="button" class="ef-ggs__go" aria-label="Glitch the current layout"><span class="ef-ggs__gotxt">Glitch</span></button>
    </div>
    <nav class="ef-ggs__nav" aria-label="Layouts"></nav>
  </div>
</div>

CSS

/* Enrichr Library · https://enrichr.dev */
/* Adapted from https://github.com/codrops/GlitchyGrid (MIT) — Copyright (c) 2009 - 2020 [Codrops](https://tympanus.net/codrops) */
.ef-ggs { width: 100%; }
.ef-ggs__wrap { --ef-acc: #e6ff55; --ef-ggs-gap: 10px; --ef-ggs-time: 2s; position: relative; box-sizing: border-box; width: 100%; min-height: 26rem; overflow: hidden; border-radius: 1rem; color: #fbf8f1; font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; background: #0c1e18; }
.ef-ggs__stage { position: absolute; inset: 1.1rem 1.1rem 3.3rem; }
.ef-ggs__slides { position: absolute; inset: 0; }
.ef-ggs__slide { position: absolute; inset: 0; display: grid; grid-template-columns: repeat(25, 1fr); grid-template-rows: repeat(25, 1fr); visibility: hidden; }
.ef-ggs__slide.is-current { visibility: visible; }
.ef-ggs__item { position: relative; will-change: transform; }
.ef-ggs__item:nth-child(1) { grid-area: 9 / 3 / 15 / 6; }
.ef-ggs__item:nth-child(2) { grid-area: 9 / 9 / 18 / 18; }
.ef-ggs__item:nth-child(3) { grid-area: 11 / 21 / 20 / 26; }
.ef-ggs__item:nth-child(4) { grid-area: 1 / 7 / 7 / 11; }
.ef-ggs__item:nth-child(5) { grid-area: 1 / 16 / 6 / 19; }
.ef-ggs__item:nth-child(6) { grid-area: 20 / 1 / 26 / 7; }
.ef-ggs__item:nth-child(7) { grid-area: 21 / 11 / 26 / 15; }
.ef-ggs__item:nth-child(8) { grid-area: 2 / 21 / 8 / 24; }
.ef-ggs__img, .ef-ggs__glitch { position: absolute; inset: 0; }
.ef-ggs__img { will-change: transform, opacity; }
.ef-ggs__glitch { opacity: 0; pointer-events: none; }
.ef-ggs__art { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; overflow: hidden; border-radius: 0.3rem; }
.ef-ggs__art svg { width: 82%; height: 82%; display: block; }
.ef-ggs__gi { position: absolute; inset: 0; transform: translate3d(0, 0, 0); }
.ef-ggs__gi:nth-child(n+2) { opacity: 0; top: -2px; left: calc(-1 * var(--ef-ggs-gap)); right: calc(-1 * var(--ef-ggs-gap)); bottom: -2px; inset: -2px calc(-1 * var(--ef-ggs-gap)); }
.ef-ggs__slide.is-glitch .ef-ggs__glitch, .ef-ggs__slide.is-glitch .ef-ggs__gi:nth-child(n+2) { opacity: 1; }
.ef-ggs__slide.is-glitch .ef-ggs__gi:nth-child(2) { transform: translate3d(var(--ef-ggs-gap), 0, 0); animation: ef-ggs-a var(--ef-ggs-time) linear alternate; }
.ef-ggs__slide.is-glitch .ef-ggs__gi:nth-child(3) { transform: translate3d(calc(-1 * var(--ef-ggs-gap)), 0, 0); animation: ef-ggs-b var(--ef-ggs-time) linear alternate; }
.ef-ggs__slide.is-glitch .ef-ggs__gi:nth-child(4) { transform: translate3d(0, -2px, 0) scale3d(-1, -1, 1); animation: ef-ggs-c var(--ef-ggs-time) linear alternate; }
.ef-ggs__slide.is-glitch .ef-ggs__gi:nth-child(5) { animation: ef-ggs-flash 0.5s steps(1, end); }
@keyframes ef-ggs-a { 0% { clip-path: polygon(0 2%, 100% 2%, 100% 6%, 0 6%); } 12% { clip-path: polygon(0 18%, 100% 18%, 100% 18%, 0 18%); } 24% { clip-path: polygon(0 10%, 100% 10%, 100% 24%, 0 24%); } 36% { clip-path: polygon(0 34%, 100% 34%, 100% 34%, 0 34%); } 50% { clip-path: polygon(0 46%, 100% 46%, 100% 46%, 0 46%); } 62% { clip-path: polygon(0 52%, 100% 52%, 100% 24%, 0 24%); } 76% { clip-path: polygon(0 72%, 100% 72%, 100% 72%, 0 72%); } 88% { clip-path: polygon(0 50%, 100% 50%, 100% 57%, 0 57%); } 100% { clip-path: polygon(0 70%, 100% 70%, 100% 82%, 0 82%); } }
@keyframes ef-ggs-b { 0% { clip-path: polygon(0 26%, 100% 26%, 100% 32%, 0 32%); } 16% { clip-path: polygon(0 4%, 100% 4%, 100% 4%, 0 4%); } 28% { clip-path: polygon(0 6%, 100% 6%, 100% 22%, 0 22%); } 40% { clip-path: polygon(0 22%, 100% 22%, 100% 22%, 0 22%); } 54% { clip-path: polygon(0 42%, 100% 42%, 100% 42%, 0 42%); } 66% { clip-path: polygon(0 54%, 100% 54%, 100% 62%, 0 62%); } 78% { clip-path: polygon(0 62%, 100% 62%, 100% 62%, 0 62%); } 90% { clip-path: polygon(0 80%, 100% 80%, 100% 90%, 0 90%); } 100% { clip-path: polygon(0 12%, 100% 12%, 100% 20%, 0 20%); } }
@keyframes ef-ggs-c { 0% { clip-path: polygon(0 60%, 100% 60%, 100% 66%, 0 66%); } 14% { clip-path: polygon(0 30%, 100% 30%, 100% 30%, 0 30%); } 30% { clip-path: polygon(0 36%, 100% 36%, 100% 50%, 0 50%); } 46% { clip-path: polygon(0 8%, 100% 8%, 100% 8%, 0 8%); } 58% { clip-path: polygon(0 74%, 100% 74%, 100% 86%, 0 86%); } 72% { clip-path: polygon(0 40%, 100% 40%, 100% 40%, 0 40%); } 86% { clip-path: polygon(0 16%, 100% 16%, 100% 24%, 0 24%); } 100% { clip-path: polygon(0 88%, 100% 88%, 100% 96%, 0 96%); } }
@keyframes ef-ggs-flash { 0% { opacity: 0; } 20% { opacity: 1; } 40% { opacity: 0; } 60% { opacity: 1; } 80% { opacity: 0; } }
.ef-ggs__text { position: absolute; right: 0; bottom: 0; width: 36%; text-align: right; pointer-events: none; }
.ef-ggs__tt { display: block; font-size: 1.3rem; font-weight: 800; letter-spacing: -0.01em; line-height: 1.1; color: var(--ef-acc); }
.ef-ggs__ts { display: block; margin-top: 0.25rem; font-size: 0.68rem; letter-spacing: 0.06em; text-transform: uppercase; opacity: 0.8; }
.ef-ggs__go { position: absolute; top: 50%; left: 50%; z-index: 5; width: 4.6rem; height: 4.6rem; margin: -2.3rem 0 0 -2.3rem; padding: 0; border: 0; border-radius: 50%; background: var(--ef-acc); color: #0c1e18; cursor: pointer; display: flex; align-items: center; justify-content: center; font: inherit; box-shadow: 0 8px 18px -8px rgba(0, 0, 0, 0.6); will-change: transform; }
.ef-ggs__gotxt { display: block; font-size: 0.62rem; font-weight: 800; letter-spacing: 0.14em; text-transform: uppercase; pointer-events: none; will-change: transform; }
.ef-ggs__go:focus-visible, .ef-ggs__dot:focus-visible { outline: 2px solid #fbf8f1; outline-offset: 3px; }
.ef-ggs__nav { position: absolute; left: 0; right: 0; bottom: 1rem; display: flex; justify-content: center; gap: 1.1rem; }
.ef-ggs__dot { position: relative; width: 1.6rem; height: 2px; padding: 0; border: 0; background: rgba(251, 248, 241, 0.45); cursor: pointer; }
.ef-ggs__dot::before { content: ""; position: absolute; inset: -0.6rem -0.2rem; }
.ef-ggs__dot[aria-current="true"] { background: var(--ef-acc); }
@media (prefers-reduced-motion: reduce) {
  .ef-ggs__slide.is-glitch .ef-ggs__gi:nth-child(n+2) { animation: none; clip-path: polygon(0 38%, 100% 38%, 100% 52%, 0 52%); }
  .ef-ggs__slide.is-glitch .ef-ggs__gi:nth-child(5) { animation: none; opacity: 0; }
}

JavaScript

// Enrichr Library · https://enrichr.dev
// Adapted from https://github.com/codrops/GlitchyGrid (MIT) — Copyright (c) 2009 - 2020 [Codrops](https://tympanus.net/codrops)
// Glitchy grid stack: a grid of puzzle-piece tiles that drift against the pointer. Switching layouts sends the tiles out, then drops the next set
// in from the centre, stacked and rotated, and settles them into the grid. A magnetic button triggers a clipped, offset-layer glitch on hover or focus.
// Vanilla port: Web Animations API for the transitions, a small rAF lerp loop for the drift and the magnet. No libraries.
// mount(root, { background, accent, parallax (px), duration (x), glitchGap (px), glitchTime (s) })
function mount(root, o) {
  var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  var wrap = root.querySelector(".ef-ggs__wrap");
  var stage = root.querySelector(".ef-ggs__stage");
  var holder = root.querySelector(".ef-ggs__slides");
  var nav = root.querySelector(".ef-ggs__nav");
  var btn = root.querySelector(".ef-ggs__go");
  var btnTxt = root.querySelector(".ef-ggs__gotxt");
  var opts = {}, slides = [], dots = [], offs = [], running = [], timers = [];
  var cur = 0, animating = false, destroyed = false, hovering = false, raf = 0, built = false;
  var ptr = null, nx = 0, ny = 0, sx = 0, sy = 0, bx = 0, by = 0;
  var PIECES = {
    1: { vb: "1513.7 1453.9 322.3 322.3", 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" },
    4: { vb: "1423.36 860.8 503.07 322.3", 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" },
    8: { vb: "1513.7 166.86 322.3 503.06", 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" }
  };
  var TITLES = [["Stacked", "Pieces fall into place"], ["Scatter", "Offset and drifting"], ["Mosaic", "Edges that interlock"]];
  var EASE_IO = "cubic-bezier(.65,0,.35,1)", EASE_OUT = "cubic-bezier(.16,1,.3,1)";

  function lerp(a, b, t) { return a + (b - a) * t; }
  function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
  function pal(a) {
    return [["#0a6b46", "#005236", a], [a, "#c6f26b", "#0c1e18"], ["#fbf8f1", "#c6f26b", "#005236"], ["#2f7d5b", "#0a6b46", "#fbf8f1"],
      ["#005236", "#0c1e18", "#c6f26b"], ["#c6f26b", "#e6ff55", "#0a6b46"], ["#0c1e18", "#005236", a], ["#fbf8f1", "#fbf8f1", "#2f7d5b"]];
  }
  function makeArt(s, i) {
    var p = pal(opts.accent)[(i + s * 3) % 8];
    var pc = PIECES[[1, 4, 8][(i + s) % 3]];
    var art = document.createElement("div");
    art.className = "ef-ggs__art";
    art.style.background = "linear-gradient(" + (145 + s * 40) + "deg, " + p[0] + ", " + p[1] + ")";
    art.style.color = p[2];
    art.innerHTML = '<svg viewBox="' + pc.vb + '" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false" style="transform:rotate(' + (((i + s) % 4) * 90) + 'deg)"><path fill="currentColor" d="' + pc.d + '"/></svg>';
    return art;
  }
  function an(el, kf, op) {
    var a = el.animate(kf, Object.assign({ fill: "both" }, op));
    running.push(a);
    return a;
  }
  function done(list) {
    return Promise.all(list.map(function (a) { return a.finished; })).then(function () {
      list.forEach(function (a) { a.cancel(); });
      return true;
    }, function () { return false; });
  }
  function cancelAll() {
    timers.forEach(clearTimeout); timers = [];
    running.forEach(function (a) { try { a.cancel(); } catch (e) {} });
    running = [];
  }
  function clear() { offs.forEach(function (f) { f(); }); offs = []; }

  function build() {
    cancelAll();
    clear();
    animating = false;
    holder.textContent = ""; nav.textContent = "";
    slides = []; dots = [];
    for (var s = 0; s < 3; s++) (function (s) {
      var el = document.createElement("div");
      el.className = "ef-ggs__slide" + (s === cur ? " is-current" : "");
      el.setAttribute("aria-hidden", s === cur ? "false" : "true");
      var items = [];
      for (var i = 0; i < 8; i++) {
        var item = document.createElement("div"); item.className = "ef-ggs__item";
        var img = document.createElement("div"); img.className = "ef-ggs__img"; img.appendChild(makeArt(s, i));
        var gl = document.createElement("div"); gl.className = "ef-ggs__glitch";
        for (var k = 0; k < 5; k++) { var gi = document.createElement("div"); gi.className = "ef-ggs__gi"; gi.appendChild(makeArt(s, i)); gl.appendChild(gi); }
        item.appendChild(img); item.appendChild(gl);
        el.appendChild(item);
        items.push({ item: item, img: img });
      }
      var tx = document.createElement("div"); tx.className = "ef-ggs__text";
      var t1 = document.createElement("span"); t1.className = "ef-ggs__tt"; t1.textContent = TITLES[s][0];
      var t2 = document.createElement("span"); t2.className = "ef-ggs__ts"; t2.textContent = TITLES[s][1];
      tx.appendChild(t1); tx.appendChild(t2);
      el.appendChild(tx);
      holder.appendChild(el);
      slides.push({ el: el, items: items, texts: [t1, t2] });
      var d = document.createElement("button");
      d.type = "button"; d.className = "ef-ggs__dot";
      d.setAttribute("aria-label", "Show layout " + (s + 1) + ": " + TITLES[s][0]);
      d.setAttribute("aria-current", s === cur ? "true" : "false");
      var onc = function () { go(s); };
      d.addEventListener("click", onc);
      offs.push(function () { d.removeEventListener("click", onc); });
      nav.appendChild(d); dots.push(d);
    })(s);
    built = true;
    kick();
  }

  function setCurrent(n) {
    slides.forEach(function (sl, i) {
      sl.el.classList.toggle("is-current", i === n);
      sl.el.setAttribute("aria-hidden", i === n ? "false" : "true");
      if (i !== n) sl.el.classList.remove("is-glitch");
    });
    dots.forEach(function (d, i) { d.setAttribute("aria-current", i === n ? "true" : "false"); });
    cur = n;
  }

  function go(next) {
    if (animating || next === cur || destroyed) return;
    if (reduce) {
      var from = slides[cur].el, to = slides[next].el;
      setCurrent(next);
      an(to, [{ opacity: 0 }, { opacity: 1 }], { duration: 180, easing: "ease-out" }).finished.then(function () { cancelAll(); }, function () {});
      return;
    }
    animating = true;
    var D = opts.duration, dir = next > cur ? 1 : -1;
    var old = slides[cur], nw = slides[next];
    var bg = opts.background, tint = "#005236";
    var out = [];
    old.items.forEach(function (it, i) {
      out.push(an(it.img, [{ transform: "translateY(0)", opacity: 1 }, { transform: "translateY(" + (dir === -1 ? 100 : -100) + "%)", opacity: 0 }], { duration: 600 * D, delay: i * 20, easing: EASE_IO }));
    });
    old.texts.forEach(function (t, i) {
      out.push(an(t, [{ transform: "translateY(0)", opacity: 1 }, { transform: "translateY(" + (dir === -1 ? 50 : -50) + "%)", opacity: 0 }], { duration: 600 * D, delay: (dir === 1 ? i : 1 - i) * 100, easing: EASE_IO }));
    });
    out.push(an(wrap, [{ backgroundColor: bg }, { backgroundColor: tint }], { duration: 400 * D, delay: 200, easing: "ease-out" }));
    done(out).then(function (ok) {
      if (!ok || destroyed) return;
      wrap.style.backgroundColor = tint;
      setCurrent(next);
      var inn = [];
      var sr = nw.el.getBoundingClientRect();
      var cx = nw.el.offsetWidth / 2, cy = nw.el.offsetHeight / 2;
      var n = nw.items.length;
      nw.items.forEach(function (it, i) {
        var x = cx - (it.item.offsetLeft + it.item.offsetWidth / 2), y = cy - (it.item.offsetTop + it.item.offsetHeight / 2);
        var rot = (((i * 53) % 21) - 10);
        var S = "translate(" + x + "px," + y + "px) rotate(" + rot + "deg) scale(2)";
        var popAt = 100 * D + i * 100 * D, settle = 100 * D + n * 100 * D * 0.7 + i * 50 * D, dur = settle + 900 * D;
        inn.push(an(it.img, [
          { opacity: 0, transform: S, offset: 0 },
          { opacity: 0, transform: S, offset: Math.max(0, popAt / dur - 0.0005) },
          { opacity: 1, transform: S, offset: popAt / dur },
          { opacity: 1, transform: S, offset: settle / dur, easing: EASE_OUT },
          { opacity: 1, transform: "translate(0,0) rotate(0deg) scale(1)", offset: 1 }
        ], { duration: dur }));
      });
      nw.texts.forEach(function (t, i) {
        inn.push(an(t, [{ transform: "translateY(" + (dir === -1 ? -50 : 50) + "%)", opacity: 0 }, { transform: "translateY(0)", opacity: 1 }], { duration: 900 * D, delay: 700 * D + (dir === 1 ? i : 1 - i) * 150 * D, easing: EASE_OUT }));
      });
      var bgA = an(wrap, [{ backgroundColor: tint }, { backgroundColor: bg }], { duration: 500 * D, delay: 200 * D, easing: "ease-out" });
      inn.push(bgA);
      done(inn).then(function (ok2) {
        wrap.style.backgroundColor = opts.background;
        if (!ok2 || destroyed) return;
        animating = false;
        kick();
      });
    });
  }

  function glitch(on) {
    if (!slides.length) return;
    slides[cur].el.classList.toggle("is-glitch", on);
  }
  function onEnter() { hovering = true; glitch(true); }
  function onLeave() { hovering = false; glitch(false); }
  function onClick() {
    glitch(true);
    var t = setTimeout(function () { if (!hovering && document.activeElement !== btn) glitch(false); }, opts.glitchTime * 1000);
    timers.push(t);
  }
  function onMove(e) {
    var r = wrap.getBoundingClientRect();
    ptr = { x: e.clientX - r.left, y: e.clientY - r.top };
    nx = (ptr.x / r.width) * 2 - 1; ny = (ptr.y / r.height) * 2 - 1;
    kick();
  }
  function onOut() { ptr = null; nx = 0; ny = 0; kick(); }
  function kick() { if (!reduce && !raf && !destroyed) raf = requestAnimationFrame(tick); }
  function tick() {
    raf = 0;
    if (destroyed || !built) return;
    sx = lerp(sx, -nx, 0.07); sy = lerp(sy, -ny, 0.07);
    var sl = slides[cur];
    sl.items.forEach(function (it, i) {
      var k = opts.parallax * (0.5 + ((i * 37) % 10) / 10);
      it.item.style.transform = "translate3d(" + (sx * k).toFixed(2) + "px," + (sy * k * 0.8).toFixed(2) + "px,0)";
    });
    var cx0 = btn.offsetLeft + btn.offsetWidth / 2, cy0 = btn.offsetTop + btn.offsetHeight / 2;
    var tx = 0, ty = 0;
    if (ptr) {
      var r = stage.getBoundingClientRect(), w = wrap.getBoundingClientRect();
      var px = ptr.x - (r.left - w.left), py = ptr.y - (r.top - w.top);
      var dx = px - cx0, dy = py - cy0;
      if (Math.sqrt(dx * dx + dy * dy) < btn.offsetWidth * 0.9) { tx = dx * 0.6; ty = dy * 0.6; }
    }
    bx = lerp(bx, tx, 0.17); by = lerp(by, ty, 0.17);
    btn.style.transform = "translate3d(" + bx.toFixed(2) + "px," + by.toFixed(2) + "px,0)";
    btnTxt.style.transform = "translate3d(" + (-bx * 0.12).toFixed(2) + "px," + (-by * 0.12).toFixed(2) + "px,0)";
    var moving = Math.abs(sx + nx) > 0.002 || Math.abs(sy + ny) > 0.002 || Math.abs(bx - tx) > 0.05 || Math.abs(by - ty) > 0.05;
    if (moving) raf = requestAnimationFrame(tick);
  }

  function update(next) {
    var prev = opts;
    opts = Object.assign({ background: "#0c1e18", accent: "#e6ff55", parallax: 20, duration: 1, glitchGap: 10, glitchTime: 2 }, opts, next || {});
    opts.parallax = clamp(+opts.parallax, 0, 40);
    opts.duration = clamp(+opts.duration, 0.4, 2);
    opts.glitchGap = clamp(+opts.glitchGap, 2, 24);
    opts.glitchTime = clamp(+opts.glitchTime, 0.5, 4);
    if (!animating) wrap.style.background = opts.background;
    wrap.style.setProperty("--ef-acc", opts.accent);
    wrap.style.setProperty("--ef-ggs-gap", opts.glitchGap + "px");
    wrap.style.setProperty("--ef-ggs-time", opts.glitchTime + "s");
    if (!built || prev.accent !== opts.accent) build();
    if (opts.parallax === 0 && built) kick();
  }

  var wire = function (el, ev, fn) { el.addEventListener(ev, fn); offs.push(function () { el.removeEventListener(ev, fn); }); };
  wire(btn, "pointerenter", onEnter);
  wire(btn, "pointerleave", onLeave);
  wire(btn, "focus", onEnter);
  wire(btn, "blur", onLeave);
  wire(btn, "click", onClick);
  wire(wrap, "pointermove", onMove);
  wire(wrap, "pointerleave", onOut);
  var baseOffs = offs; offs = [];
  update(o);
  return {
    update: update,
    destroy: function () {
      destroyed = true;
      if (raf) cancelAnimationFrame(raf);
      raf = 0;
      cancelAll();
      clear();
      baseOffs.forEach(function (f) { f(); });
    }
  };
}

// Usage
var fx = mount(document.querySelector("#my-root"), {"accent":"#e6ff55","parallax":20,"duration":1,"glitchGap":10,"glitchTime":2});

How it works

This is a vanilla JS and CSS port of the Codrops Glitchy Grid idea, with no GSAP. Eight tiles sit in a 25 by 25 CSS grid. A small requestAnimationFrame loop lerps each tile toward the opposite side of the pointer by a different amount, so the grid drifts in layers, and the same loop makes the round button follow the pointer when it is close. Changing layout uses the Web Animations API: the current tiles slide away in a stagger, then the next tiles are placed at the centre of the grid, scaled and rotated, pop in one by one, and spring back to their cells with an expo ease. The glitch is five copies of each tile stacked on top of each other; three of them are offset sideways or flipped and animated through different clip-path bands for the duration you set.

Accessibility and performance

Layouts switch with real buttons that carry labels and aria-current, and the glitch button works on keyboard focus as well as hover. Hidden layouts are aria-hidden. With reduced motion the drift and magnet are off, layouts swap with a short fade, and the glitch is a still offset band instead of an animation. The glitch and fast cuts can bother some people, so keep it off pages where that matters.

Only transform, opacity and clip-path are animated. The drift loop writes eight transforms per frame and stops itself once the tiles settle. Each tile holds six small inline SVG layers, so keep the tile count modest if you add more.

SectionGallery
TechniquesPointer interaction, Stagger, Reveal and intro
StackVanilla JS, CSS, Web Animations API
Works withReact, Next.js, Vanilla
DependenciesNone
Statustested
Inspired byGlitchy Grid Layout Slideshow 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.

Image trail introAn intro where gradient tiles are emitted along the pointer, or along a looping path when idle, stack and fade, and then the headline slides in. Expanding rounded menuA round menu button that grows into a full rounded panel with a clip-path reveal and staggered links, with Esc, outside-click and a focus trap. Stagger letter swapClick to swap two screens of big type: every character slides up and out in a stagger, the background shifts colour, and the next text rises into place, with a soft cursor circle following the mouse.

FAQ

Can I use photos instead of puzzle pieces?

Yes. Replace the art element inside each tile with an image or a background-image. The glitch layers clone whatever the tile holds, so they pick it up automatically.

How do I change the layout of the tiles?

The tile positions are the grid-area rules on .ef-ggs__item. Edit those eight lines and the transition, which measures each tile from the centre, follows along.

Does it need GSAP?

No. The transitions use element.animate with stagger delays computed in the script, and the drift is a short lerp loop.