Copied!
Full page

Grid overlay explainer

Column and gutter bands fade in over a live layout as it scrolls into view, to show how the grid is built.

← Library

Element · Vanilla JS + CSSUpdated November 5, 2026

HeroElementVanilla 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 "Grid overlay explainer" for my site using the Enrichr Library element at https://enrichr.dev/elements/grid-overlay-explainer.html.

Read the machine-readable spec and source at https://enrichr.dev/elements/grid-overlay-explainer.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:
- columns: 12
- gutter: 16
- margin: 24
- maxOpacity: 1
- manual: 0
- bandColor: #ff5a36
- blockColor: #c6f26b
- textColor: #10261f

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/grid-overlay-explainer.json.

Code

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

HTML

<div class="ef-grid">
  <div class="ef-grid__layout"></div>
  <div class="ef-grid__bands" aria-hidden="true"></div>
  <span class="ef-grid__tag" aria-hidden="true"></span>
</div>

CSS

.ef-grid { position: relative; padding: 0 var(--ef-margin, 24px); min-height: 14rem; --ef-cols: 12; --ef-gutter: 16px; }
.ef-grid__layout, .ef-grid__bands { display: grid; grid-template-columns: repeat(var(--ef-cols), minmax(0, 1fr)); gap: var(--ef-gutter); }
.ef-grid__layout { position: relative; z-index: 1; padding: 1.2rem 0; row-gap: var(--ef-gutter); }
.ef-grid__block { border-radius: 0.5rem; min-height: 3.4rem; display: flex; align-items: flex-end; padding: 0.5rem 0.7rem; font-size: 0.8rem; font-weight: 600; color: var(--ef-ink, #10261f); background: var(--ef-block, #c6f26b); overflow: hidden; }
.ef-grid__bands { position: absolute; z-index: 2; inset: 0 var(--ef-margin, 24px); pointer-events: none; opacity: var(--ef-o, 1); }
.ef-grid__band { background: color-mix(in srgb, var(--ef-band-hex, #ff5a36) 32%, transparent); border-inline: 1px solid var(--ef-band-hex, #ff5a36); }
.ef-grid__tag { position: absolute; z-index: 3; right: var(--ef-margin, 24px); top: 0.3rem; font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 0.7rem; padding: 0.15rem 0.5rem; border-radius: 100rem; background: #fff; color: #000; opacity: var(--ef-o, 1); }

JavaScript

// Grid overlay explainer: column and gutter bands fade in over a live layout as it scrolls into view, to show how the grid is built.
// The layout and the bands read the same CSS custom properties, so they can never disagree.
// mount(root, { columns (4-12), gutter (px), margin (px), maxOpacity (0-1), manual (0 = follow scroll, 1-100 = fixed %), bandColor, blockColor, textColor })
function mount(root, o) {
  var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  var box = root.querySelector(".ef-grid");
  var layout = root.querySelector(".ef-grid__layout");
  var bands = root.querySelector(".ef-grid__bands");
  var tag = root.querySelector(".ef-grid__tag");
  var opts = {}, raf = 0;

  // block spans for a 12 column grid; scaled down for fewer columns
  var SPANS = [[12, "Header"], [8, "Main"], [4, "Aside"], [4, "A"], [4, "B"], [4, "C"]];
  function render() {
    var cols = Math.max(2, Math.min(12, opts.columns));
    box.style.setProperty("--ef-cols", cols);
    box.style.setProperty("--ef-gutter", opts.gutter + "px");
    box.style.setProperty("--ef-margin", opts.margin + "px");
    box.style.setProperty("--ef-band-hex", opts.bandColor);
    box.style.setProperty("--ef-block", opts.blockColor);
    box.style.setProperty("--ef-ink", opts.textColor);
    layout.textContent = ""; bands.textContent = "";
    SPANS.forEach(function (s) {
      var d = document.createElement("div");
      d.className = "ef-grid__block";
      d.style.gridColumn = "span " + Math.max(1, Math.round((s[0] / 12) * cols));
      d.textContent = s[1];
      layout.appendChild(d);
    });
    for (var i = 0; i < cols; i++) { var b = document.createElement("div"); b.className = "ef-grid__band"; bands.appendChild(b); }
    tag.textContent = cols + " columns · " + opts.gutter + "px gutter · " + opts.margin + "px margin";
    layout.setAttribute("role", "group");
    layout.setAttribute("aria-label", "Example layout on a " + cols + " column grid");
    paint();
  }
  function paint() {
    raf = 0;
    var p = 1;
    if (!reduce) {
      if (opts.manual > 0) p = opts.manual / 100;
      else { var r = box.getBoundingClientRect(), vh = window.innerHeight || 800; p = Math.min(1, Math.max(0, (vh * 0.9 - r.top) / (vh * 0.5))); }
    }
    box.style.setProperty("--ef-o", String((p * opts.maxOpacity).toFixed(3)));
  }
  function schedule() { if (!raf) raf = requestAnimationFrame(paint); }
  function update(next) {
    opts = Object.assign({ columns: 12, gutter: 16, margin: 24, maxOpacity: 1, manual: 0, bandColor: "#ff5a36", blockColor: "#c6f26b", textColor: "#10261f" }, opts, next || {});
    render();
  }
  update(o);
  if (!reduce) { window.addEventListener("scroll", schedule, { passive: true }); window.addEventListener("resize", schedule); }
  return { update: update, destroy: function () { cancelAnimationFrame(raf); window.removeEventListener("scroll", schedule); window.removeEventListener("resize", schedule); } };
}

// Usage
var fx = mount(document.querySelector("#my-root"), {"columns":12,"gutter":16,"margin":24,"maxOpacity":1,"manual":0,"bandColor":"#ff5a36","blockColor":"#c6f26b","textColor":"#10261f"});

How it works

The layout and the overlay are two CSS grids that read the same custom properties for column count, gutter and margin, so the bands always sit exactly on the columns and cannot drift out of sync. Scroll progress sets one more property, the overlay opacity, so the bands fade in as the layout moves into view. Changing the columns control changes the grid for both at once.

Accessibility and performance

The bands are decorative and hidden from assistive technology; the example layout is a labelled group. With reduced motion the overlay is fully visible and does not fade. Do not rely on colour alone to explain the grid in real documentation.

One bounding-rect read per scroll frame and one custom property write. No animation library and no layout changes while it runs.

SectionFull page
TechniquesScroll-driven
StackVanilla JS, CSS
Works withReact, Next.js, Vanilla
DependenciesNone
Statustested

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.

Word read highlightWords in a paragraph light up one by one as it scrolls through the viewport. Scroll velocity skewText that leans and splits into two colour fringes in proportion to how fast the page scrolls. Scroll spring headingAn oversized heading slides sideways with scroll progress through a spring, lagging and settling.

FAQ

Can I overlay my real page grid?

Yes. Give your page container the same custom properties and place the bands element over it. The bands only need the column count, gutter and margin.

Why use CSS variables for both?

A shared source of truth means the explanation can never show a different grid from the one the layout uses.

Does it work for fewer than 12 columns?

Yes. The example blocks scale their spans to the column count you choose.