Copied!
Gallery

Cutout collage grid

An editorial collage built with CSS grid and clip-path shapes over generated art, with a gentle depth drift on pointer move.

← Library

Element · Vanilla JS + CSSUpdated November 5, 2026

3DElementVanilla 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 "Cutout collage grid" for my site using the Enrichr Library element at https://enrichr.dev/elements/cutout-collage-grid.html.

Read the machine-readable spec and source at https://enrichr.dev/elements/cutout-collage-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:
- background: #fbf8f1
- colorA: #005236
- colorB: #e6ff55
- title: Cut, paste, compose
- parallax: 1

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/cutout-collage-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-ccg">
  <div class="ef-ccg__wrap">
    <div class="ef-ccg__grid">
      <div class="ef-ccg__tile ef-ccg__t1" data-d="1"><div class="ef-ccg__art" aria-hidden="true"></div></div>
      <div class="ef-ccg__tile ef-ccg__t2" data-d="1.6"><div class="ef-ccg__art" aria-hidden="true"></div></div>
      <div class="ef-ccg__tile ef-ccg__t3" data-d="0.8"><div class="ef-ccg__art" aria-hidden="true"></div></div>
      <div class="ef-ccg__tile ef-ccg__t4" data-d="0.5"><div class="ef-ccg__art" aria-hidden="true"></div><p class="ef-ccg__title"></p><p class="ef-ccg__sub"></p></div>
      <div class="ef-ccg__tile ef-ccg__t5" data-d="1.3"><div class="ef-ccg__art" aria-hidden="true"></div></div>
    </div>
  </div>
</div>

CSS

/* Enrichr Library · https://enrichr.dev */
.ef-ccg { width: 100%; container-type: inline-size; }
.ef-ccg__wrap { box-sizing: border-box; width: 100%; min-height: 16rem; padding: clamp(0.6rem, 3cqi, 1.6rem); border-radius: 1rem; display: flex; align-items: center; justify-content: center; font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
.ef-ccg__grid { width: 100%; max-width: 46rem; display: grid; gap: clamp(0.25rem, 1.2cqi, 0.6rem); grid-template-columns: repeat(11, 1fr); grid-template-rows: repeat(8, 1fr); aspect-ratio: 11 / 8; }
.ef-ccg__tile { position: relative; overflow: hidden; will-change: transform; }
.ef-ccg__art { position: absolute; inset: -10%; will-change: transform; }
.ef-ccg__t1 { grid-area: 1 / 1 / 5 / 5; clip-path: circle(50% at 50% 50%); }
.ef-ccg__t2 { grid-area: 1 / 5 / 9 / 8; clip-path: inset(0 round 999px 999px 0 0); }
.ef-ccg__t3 { grid-area: 1 / 8 / 5 / 12; clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%); }
.ef-ccg__t4 { grid-area: 5 / 1 / 9 / 5; clip-path: inset(0 round 12% 12% 12% 12% / 16% 16% 16% 16%); display: flex; flex-direction: column; justify-content: flex-end; padding: 8%; box-sizing: border-box; }
.ef-ccg__t5 { grid-area: 5 / 8 / 9 / 12; clip-path: polygon(25% 3%, 75% 3%, 100% 50%, 75% 97%, 25% 97%, 0 50%); }
.ef-ccg__title { position: relative; margin: 0; font-size: clamp(0.8rem, 5.2cqi, 2rem); font-weight: 800; line-height: 1; letter-spacing: -0.02em; }
.ef-ccg__sub { position: relative; margin: 0.4em 0 0; font-size: clamp(0.5rem, 1.9cqi, 0.8rem); letter-spacing: 0.1em; text-transform: uppercase; opacity: 0.7; }
@container (max-width: 34rem) {
  .ef-ccg__grid { grid-template-columns: repeat(6, 1fr); grid-template-rows: repeat(9, 1fr); aspect-ratio: 6 / 9; }
  .ef-ccg__t1 { grid-area: 1 / 1 / 4 / 4; }
  .ef-ccg__t3 { grid-area: 1 / 4 / 4 / 7; }
  .ef-ccg__t2 { grid-area: 4 / 1 / 10 / 3; }
  .ef-ccg__t4 { grid-area: 4 / 3 / 7 / 7; }
  .ef-ccg__t5 { grid-area: 7 / 3 / 10 / 7; }
  .ef-ccg__title { font-size: clamp(0.8rem, 7cqi, 1.6rem); }
  .ef-ccg__sub { font-size: 0.5rem; }
}

JavaScript

// Enrichr Library · https://enrichr.dev
// Cutout collage grid: an editorial layout of CSS grid tiles cut into a circle, an arch, a diamond, a rounded card and a hexagon with clip-path.
// The artwork is drawn with CSS gradients. On pointer move each tile drifts by its own depth while the art inside slides the other way,
// so the cutouts feel like windows. Reduced motion and touch screens show the still layout. The grid reflows for narrow containers.
// mount(root, { background, colorA, colorB, ink, title, subtitle, parallax (0-2) })
function mount(root, o) {
  var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  var wrap = root.querySelector(".ef-ccg__wrap");
  var tiles = Array.prototype.slice.call(root.querySelectorAll(".ef-ccg__tile"));
  var arts = tiles.map(function (t) { return t.querySelector(".ef-ccg__art"); });
  var depths = tiles.map(function (t) { return parseFloat(t.getAttribute("data-d")) || 1; });
  var title = root.querySelector(".ef-ccg__title");
  var sub = root.querySelector(".ef-ccg__sub");
  var opts = {}, raf = 0, visible = true, io = null, tx = 0, ty = 0, cx = 0, cy = 0;

  function paintArt() {
    var a = opts.colorA, b = opts.colorB, c = "#fbf8f1", d = opts.ink;
    arts[0].style.background = "repeating-conic-gradient(from 0deg at 50% 50%, " + a + " 0 10deg, " + d + " 10deg 20deg)";
    arts[1].style.background = "radial-gradient(circle at 50% 30%, " + b + " 0 16%, transparent 16.4%), repeating-linear-gradient(180deg, " + a + " 0 18px, " + c + " 18px 26px)";
    arts[2].style.background = "radial-gradient(circle, " + d + " 0 3px, transparent 3.5px) 0 0 / 16px 16px, " + b;
    arts[3].style.background = d;
    arts[4].style.background = "repeating-radial-gradient(circle at 50% 50%, " + b + " 0 12px, " + a + " 12px 24px)";
    tiles[3].style.color = c;
    tiles[0].style.background = tiles[1].style.background = tiles[2].style.background = tiles[4].style.background = "transparent";
  }
  function paint() {
    var k = opts.parallax;
    for (var i = 0; i < tiles.length; i++) {
      var dd = depths[i] * k;
      tiles[i].style.transform = "translate3d(" + (cx * dd * 5).toFixed(2) + "px," + (cy * dd * 5).toFixed(2) + "px,0)";
      arts[i].style.transform = "translate3d(" + (-cx * dd * 14).toFixed(2) + "px," + (-cy * dd * 14).toFixed(2) + "px,0)";
    }
  }
  function frame() {
    raf = 0;
    cx += (tx - cx) * 0.08; cy += (ty - cy) * 0.08;
    paint();
    if (Math.abs(tx - cx) > 0.001 || Math.abs(ty - cy) > 0.001) wake();
  }
  function wake() { if (!raf && visible && !reduce) raf = requestAnimationFrame(frame); }
  function move(e) {
    if (e.pointerType === "touch") return;
    var r = wrap.getBoundingClientRect();
    tx = ((e.clientX - r.left) / r.width - 0.5) * 2;
    ty = ((e.clientY - r.top) / r.height - 0.5) * 2;
    wake();
  }
  function leave() { tx = 0; ty = 0; wake(); }
  function update(next) {
    opts = Object.assign({ background: "#fbf8f1", colorA: "#005236", colorB: "#e6ff55", ink: "#0c1e18", title: "Cut, paste, compose", subtitle: "Collage layout", parallax: 1 }, opts, next || {});
    wrap.style.background = opts.background;
    title.textContent = opts.title;
    sub.textContent = opts.subtitle;
    sub.style.color = opts.colorB;
    paintArt();
    paint();
  }
  update(o);
  if (!reduce) {
    wrap.addEventListener("pointermove", move);
    wrap.addEventListener("pointerleave", leave);
    if ("IntersectionObserver" in window) {
      io = new IntersectionObserver(function (en) { visible = en[0].isIntersecting; if (visible) wake(); else { cancelAnimationFrame(raf); raf = 0; } });
      io.observe(wrap);
    }
  }
  return {
    update: update,
    destroy: function () {
      cancelAnimationFrame(raf); raf = 0;
      if (io) io.disconnect();
      wrap.removeEventListener("pointermove", move);
      wrap.removeEventListener("pointerleave", leave);
    }
  };
}

// Usage
var fx = mount(document.querySelector("#my-root"), {"background":"#fbf8f1","colorA":"#005236","colorB":"#e6ff55","title":"Cut, paste, compose","parallax":1});

How it works

The layout is one grid with equal square cells, so a tile that spans the same number of columns and rows is square and a circle clip stays round. Each tile gets its own clip-path: circle, an inset with large top radii for the arch, a four point polygon for the diamond, an inset with a rounded corner radius, and a six point polygon for the hexagon. The art is drawn with conic, linear and radial gradients inside an oversized child. On pointer move, each tile drifts by a depth factor and its art slides the opposite way, so it reads as a window onto a layer behind. A container query switches to a narrower arrangement in small previews.

Accessibility and performance

The art tiles are decorative and hidden from assistive technology. The headline is real text with a strong contrast pairing. Parallax runs for mouse and pen only and is removed for reduced motion, which leaves the still collage. Replace the gradients with real images and give meaningful ones alt text.

Transforms only, written inside one requestAnimationFrame loop that sleeps when the pointer stops and when the element is off screen. Clip-path on moving elements is repainted each frame, so keep the tile count modest.

SectionGallery
TechniquesParallax and depth, Pointer interaction
StackVanilla JS, CSS
Works withReact, Next.js, Vanilla
DependenciesNone
Statustested
Inspired byCrafting a Cutout Collage Layout with CSS Grid and Clip-path by Bri Camp Gomez 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.

Depth parallax cardLayered shapes shift by different amounts as the pointer moves, drifting on touch screens. Depth map shaderA WebGL shader that shifts an image by its own depth and the pointer, so near parts move more than far ones. Light trails canvasStreaks of coloured light rushing toward you along a night road, drawn with additive blending on a 2D canvas.

FAQ

How do I add more shapes?

Add a tile, place it with grid-area, and give it a clip-path. Any polygon() or inset() works, and tools such as Clippy can generate the points.

Why do the shapes stay undistorted?

The grid has fixed aspect ratio and equal rows and columns, so square spans give square cells. Rectangular spans stretch percentage based shapes, which is fine for the arch and hexagon.

Can the tiles be links?

Yes. Make each tile an anchor. Clip-path also clips the pointer hit area, so clicks outside the shape pass through.