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 "Shape clip-path slideshow" for my site using the Enrichr Library element at https://enrichr.dev/elements/shape-clip-slideshow.html.
Read the machine-readable spec and source at https://enrichr.dev/elements/shape-clip-slideshow.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
- size: 0.85
- duration: 900
- autoplay: 0
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/shape-clip-slideshow.json.
Code
Mount it on an element that contains the markup. It returns an object with update(options) and destroy().
HTML
<div class="ef-scs">
<div class="ef-scs__wrap"></div>
</div>CSS
/* Enrichr Library · https://enrichr.dev */
.ef-scs { width: 100%; }
.ef-scs__wrap { box-sizing: border-box; position: relative; width: 100%; min-height: 22rem; padding: 1.1rem; border-radius: 1rem; overflow: hidden; display: flex; flex-direction: column; gap: 0.9rem; font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; color: #fbf8f1; }
.ef-scs__stage { position: relative; flex: 1 1 auto; min-height: 15rem; outline: none; }
.ef-scs__slide { position: absolute; inset: 0; opacity: 0; visibility: hidden; will-change: clip-path; }
.ef-scs__slide.is-on { opacity: 1; visibility: visible; }
.ef-scs__art { position: absolute; inset: 0; will-change: transform; }
.ef-scs__art::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, rgba(0, 0, 0, 0.1), transparent 40%, rgba(0, 0, 0, 0.35)); }
.ef-scs__bar { display: flex; align-items: center; gap: 0.8rem; }
.ef-scs__cap { flex: 1 1 auto; min-width: 0; }
.ef-scs__t { display: block; font-size: 1.05rem; font-weight: 800; letter-spacing: -0.01em; line-height: 1.15; }
.ef-scs__s { display: block; margin-top: 0.15rem; font-size: 0.72rem; letter-spacing: 0.04em; opacity: 0.75; }
.ef-scs__dots { display: flex; gap: 0.2rem; }
.ef-scs__dot { box-sizing: border-box; width: 1.5rem; height: 1.5rem; padding: 0; border: 0; background: none; display: grid; place-items: center; cursor: pointer; border-radius: 50%; }
.ef-scs__dot::before { content: ""; width: 0.55rem; height: 0.55rem; border-radius: 50%; background: rgba(251, 248, 241, 0.35); transition: transform 250ms, background 250ms; }
.ef-scs__dot[aria-current="true"]::before { background: var(--ef-acc, #e6ff55); transform: scale(1.35); }
.ef-scs__btn { box-sizing: border-box; width: 2.4rem; height: 2.4rem; padding: 0; border-radius: 50%; border: 1.5px solid rgba(251, 248, 241, 0.4); background: transparent; color: inherit; display: grid; place-items: center; cursor: pointer; transition: background 200ms, color 200ms, border-color 200ms; }
.ef-scs__btn:hover { background: var(--ef-acc, #e6ff55); color: #0c1e18; border-color: var(--ef-acc, #e6ff55); }
.ef-scs__btn svg { width: 1.1rem; height: 1.1rem; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.ef-scs__btn:focus-visible, .ef-scs__dot:focus-visible { outline: 2px solid var(--ef-acc, #e6ff55); outline-offset: 2px; }
.ef-scs__stage:focus-visible { outline: 2px solid var(--ef-acc, #e6ff55); outline-offset: 4px; border-radius: 0.5rem; }
@media (prefers-reduced-motion: reduce) { .ef-scs__dot::before, .ef-scs__btn { transition: none; } }JavaScript
// Enrichr Library · https://enrichr.dev
// Shape clip slideshow: gradient slides revealed through a clip-path polygon that morphs between a circle, diamond, hexagon and star.
// Every shape is sampled to the same 60 points so the polygons interpolate cleanly. The incoming slide grows out of the outgoing shape while the outgoing one morphs into it.
// Prev/next, dots and arrow keys. Reduced motion swaps instantly and never autoplays. Autoplay pauses on hover and focus.
// mount(root, { background, accent, size (0.3-1), duration (ms), autoplay (seconds, 0 = off) })
function mount(root, o) {
var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
var wrap = root.querySelector(".ef-scs__wrap");
var N = 60, PI = Math.PI;
var DATA = [["Circle", "Soft focus"], ["Diamond", "Hard edge"], ["Hexagon", "Cell grid"], ["Star", "Five points"], ["Circle", "Round trip"]];
var U = [0, 1, 2, 3].map(unit);
var opts = {}, built = false, cur = 0, w = 0, h = 0, raf = 0, timer = 0, anim = null, paused = false;
var stage, capEl, slides = [], arts = [], dots = [], tEl, sEl, prevB, nextB, ro, offs = [];
function unit(s) {
var out = new Array(N * 2), i, k, a, V, vs = [], t, f, p, q, rr;
if (s === 0) { for (k = 0; k < N; k++) { a = -PI / 2 + 2 * PI * k / N; out[2 * k] = Math.cos(a); out[2 * k + 1] = Math.sin(a); } return out; }
V = s === 1 ? 4 : s === 2 ? 6 : 10;
for (i = 0; i < V; i++) { a = -PI / 2 + 2 * PI * i / V; rr = (s === 3 && i % 2) ? 0.45 : 1; vs.push([Math.cos(a) * rr, Math.sin(a) * rr]); }
for (k = 0; k < N; k++) {
t = k / N * V; i = Math.floor(t); f = t - i; p = vs[i]; q = vs[(i + 1) % V];
out[2 * k] = p[0] + (q[0] - p[0]) * f; out[2 * k + 1] = p[1] + (q[1] - p[1]) * f;
}
return out;
}
function clip(A, ra, B, rb, m) {
var R = Math.min(w, h) / 2, cx = w / 2, cy = h / 2, s = "polygon(", k, x, y;
for (k = 0; k < N; k++) {
x = cx + (A[2 * k] * ra * (1 - m) + B[2 * k] * rb * m) * R;
y = cy + (A[2 * k + 1] * ra * (1 - m) + B[2 * k + 1] * rb * m) * R;
s += (k ? "," : "") + x.toFixed(1) + "px " + y.toFixed(1) + "px";
}
return s + ")";
}
function shapeOf(i) { return i % 4; }
function art(i, a) {
var c = "#fbf8f1", d = "#0c1e18", p = i % 5;
if (p === 0) return "radial-gradient(circle at 70% 30%, " + a + " 0 16%, transparent 17%), linear-gradient(160deg, #0a6b46, #005236 55%, " + d + ")";
if (p === 1) return "conic-gradient(from 210deg at 40% 60%, #005236, " + a + ", " + c + ", #005236)";
if (p === 2) return "repeating-linear-gradient(120deg, #005236 0 14px, " + d + " 14px 28px)";
if (p === 3) return "radial-gradient(circle at 30% 35%, " + a + " 0 18%, transparent 19%), radial-gradient(circle at 64% 64%, " + c + " 0 18%, transparent 19%), linear-gradient(200deg, #0a6b46, " + d + ")";
return "radial-gradient(circle, " + a + " 0 2.5px, transparent 3px) 0 0 / 18px 18px, linear-gradient(145deg, #005236, " + d + ")";
}
function icon(d) {
var ns = "http://www.w3.org/2000/svg", s = document.createElementNS(ns, "svg"), p = document.createElementNS(ns, "path");
s.setAttribute("viewBox", "0 0 24 24"); s.setAttribute("aria-hidden", "true");
p.setAttribute("d", d); s.appendChild(p);
return s;
}
function rest(i) { return clip(U[shapeOf(i)], opts.size, U[shapeOf(i)], opts.size, 0); }
function measure() {
var r = stage.getBoundingClientRect();
w = r.width; h = r.height;
if (!anim && w > 1) slides[cur].style.clipPath = rest(cur);
}
function label(i) {
tEl.textContent = DATA[i][0];
sEl.textContent = DATA[i][1] + " · " + (i + 1) + " / " + slides.length;
dots.forEach(function (d, j) { d.setAttribute("aria-current", j === i ? "true" : "false"); });
slides.forEach(function (s, j) { s.setAttribute("aria-hidden", j === i ? "false" : "true"); });
}
function settle(to) {
slides.forEach(function (s, i) {
s.classList.toggle("is-on", i === to); s.style.zIndex = i === to ? "2" : "0"; s.style.opacity = "";
arts[i].style.transform = "";
});
cur = to;
if (w > 1) slides[cur].style.clipPath = rest(cur);
label(cur);
}
function finish() {
if (!anim) return;
cancelAnimationFrame(raf); var to = anim.to; anim = null; settle(to);
}
function go(to) {
var n = slides.length;
to = (to + n) % n;
finish();
if (to === cur) return;
if (reduce || opts.duration < 30 || w < 2) { settle(to); schedule(); return; }
var from = cur, sf = U[shapeOf(from)], st = U[shapeOf(to)], t0 = performance.now(), dur = opts.duration;
slides[to].classList.add("is-on"); slides[to].style.zIndex = "2"; slides[from].style.zIndex = "1";
anim = { to: to };
label(to);
function tick(now) {
var t = Math.min(1, (now - t0) / dur), e = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
slides[to].style.clipPath = clip(sf, 0.02, st, opts.size, e);
slides[from].style.clipPath = clip(sf, opts.size, st, opts.size * (1 + 0.08 * Math.sin(PI * e)), e);
slides[from].style.opacity = String(1 - Math.max(0, (e - 0.55) / 0.45));
arts[to].style.transform = "scale(" + (1.2 - 0.2 * e).toFixed(3) + ") rotate(" + ((1 - e) * 6).toFixed(2) + "deg)";
if (t < 1) raf = requestAnimationFrame(tick);
else { anim = null; settle(to); schedule(); }
}
raf = requestAnimationFrame(tick);
}
function schedule() {
clearTimeout(timer);
if (reduce || !opts.autoplay || paused) return;
timer = setTimeout(function () { go(cur + 1); }, opts.autoplay * 1000);
}
function on(el, ev, fn) { el.addEventListener(ev, fn); offs.push(function () { el.removeEventListener(ev, fn); }); }
function build() {
wrap.textContent = "";
wrap.setAttribute("role", "group"); wrap.setAttribute("aria-roledescription", "carousel"); wrap.setAttribute("aria-label", "Shape slideshow");
stage = document.createElement("div"); stage.className = "ef-scs__stage"; stage.tabIndex = 0;
stage.setAttribute("aria-label", "Slides. Use the left and right arrow keys to change slide.");
DATA.forEach(function (d, i) {
var s = document.createElement("div"), a = document.createElement("div");
s.className = "ef-scs__slide";
s.setAttribute("role", "group"); s.setAttribute("aria-roledescription", "slide"); s.setAttribute("aria-label", (i + 1) + " of " + DATA.length + ": " + d[0]);
a.className = "ef-scs__art";
s.appendChild(a); stage.appendChild(s); slides.push(s); arts.push(a);
});
var bar = document.createElement("div"); bar.className = "ef-scs__bar";
prevB = document.createElement("button"); prevB.type = "button"; prevB.className = "ef-scs__btn"; prevB.setAttribute("aria-label", "Previous slide"); prevB.appendChild(icon("M15 5l-7 7 7 7"));
nextB = document.createElement("button"); nextB.type = "button"; nextB.className = "ef-scs__btn"; nextB.setAttribute("aria-label", "Next slide"); nextB.appendChild(icon("M9 5l7 7-7 7"));
capEl = document.createElement("div"); capEl.className = "ef-scs__cap";
tEl = document.createElement("span"); tEl.className = "ef-scs__t"; sEl = document.createElement("span"); sEl.className = "ef-scs__s";
capEl.appendChild(tEl); capEl.appendChild(sEl);
var dw = document.createElement("div"); dw.className = "ef-scs__dots";
DATA.forEach(function (d, i) {
var b = document.createElement("button"); b.type = "button"; b.className = "ef-scs__dot"; b.setAttribute("aria-label", "Go to slide " + (i + 1));
on(b, "click", function () { go(i); });
dw.appendChild(b); dots.push(b);
});
bar.appendChild(capEl); bar.appendChild(dw); bar.appendChild(prevB); bar.appendChild(nextB);
wrap.appendChild(stage); wrap.appendChild(bar);
on(prevB, "click", function () { go(cur - 1); });
on(nextB, "click", function () { go(cur + 1); });
on(wrap, "keydown", function (e) {
if (e.key === "ArrowLeft") { e.preventDefault(); go(cur - 1); }
else if (e.key === "ArrowRight") { e.preventDefault(); go(cur + 1); }
});
function pz() { paused = true; clearTimeout(timer); }
function rz() { paused = false; schedule(); }
on(wrap, "pointerenter", pz); on(wrap, "pointerleave", rz); on(wrap, "focusin", pz); on(wrap, "focusout", rz);
if (window.ResizeObserver) { ro = new ResizeObserver(measure); ro.observe(stage); }
else { on(window, "resize", measure); }
built = true;
}
function update(next) {
opts = Object.assign({ background: "#0c1e18", accent: "#e6ff55", size: 0.85, duration: 900, autoplay: 0 }, opts, next || {});
opts.size = Math.max(0.3, Math.min(1, opts.size));
wrap.style.background = opts.background;
wrap.style.setProperty("--ef-acc", opts.accent);
if (!built) build();
arts.forEach(function (a, i) { a.style.background = art(i, opts.accent); });
capEl.setAttribute("aria-live", opts.autoplay ? "off" : "polite");
finish(); measure(); settle(cur); schedule();
}
function destroy() {
cancelAnimationFrame(raf); clearTimeout(timer); anim = null;
if (ro) ro.disconnect();
offs.forEach(function (f) { f(); }); offs = [];
}
update(o);
return { update: update, destroy: destroy };
}
// Usage
var fx = mount(document.querySelector("#my-root"), {"accent":"#e6ff55","size":0.85,"duration":900,"autoplay":0});How it works
Every shape is built from the same 60 points, walked around its outline starting at the top, so any two shapes can be blended by interpolating point by point. A clip-path polygon() string is written each frame from that blend. The incoming slide starts as a dot in the shape of the outgoing slide and grows into its own shape while the outgoing slide morphs into it and fades, and the incoming artwork settles from a slight zoom and rotation. At rest each slide sits inside its own shape, sized to the stage by the size option.
Accessibility and performance
The wrapper is a labelled carousel group, each slide has a role and a position label, and only the current slide is exposed to assistive tech. Prev, next and the dots are real buttons with labels and focus rings, and the left and right arrow keys work anywhere inside the component. The caption is a polite live region that switches off during autoplay. Autoplay is off by default, pauses on hover and focus, and never runs under reduced motion, where slides swap instantly.
The animation rewrites one clip-path on two slides per frame, which is cheap for a 60 point polygon. Only the stage is observed for resize. Timers and animation frames are cancelled on destroy. Avoid layering heavy filters on the slides, as clip-path animation repaints them.
| Section | Gallery |
|---|---|
| Techniques | Carousel, Reveal and intro |
| Stack | Vanilla JS, CSS |
| Works with | React, Next.js, Vanilla |
| Dependencies | None |
| Status | tested |
| Inspired by | Shape Slideshow with Clip-path by Manoela Ilic on Codrops. 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
Why must every shape have the same number of points?
CSS and script can only blend two polygons smoothly when they have the same number of vertices. Sampling every shape to 60 points keeps circle, diamond, hexagon and star interchangeable.
Can I use photos instead of gradients?
Yes. Set a background image on the .ef-scs__art layers instead of the gradients. The clip-path is applied to the slide, so any content inside is revealed through the shape.
Can I add another shape?
Add a case to the unit() function that returns 120 numbers (60 x,y pairs) in the range -1 to 1, then extend shapeOf() to use it.