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.
| Section | Gallery |
|---|---|
| Techniques | Pointer interaction, Stagger, Reveal and intro |
| Stack | Vanilla JS, CSS, Web Animations API |
| Works with | React, Next.js, Vanilla |
| Dependencies | None |
| Status | tested |
| Inspired by | Glitchy 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.
More elements
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.