Copied!
Hero

Rapid layers intro

A stack of full-bleed colour layers wipes in quickly one after another, then wipes away to reveal a headline, with a replay button.

← 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 "Rapid layers intro" for my site using the Enrichr Library element at https://enrichr.dev/elements/rapid-layers-intro.html.

Read the machine-readable spec and source at https://enrichr.dev/elements/rapid-layers-intro.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:
- title: Make it move
- direction: right
- layers: 5
- colorA: #005236
- colorB: #e6ff55

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/rapid-layers-intro.json.

Code

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

HTML

<div class="ef-rli">
  <div class="ef-rli__stage">
    <div class="ef-rli__copy">
      <p class="ef-rli__eye">Introducing</p>
      <h3 class="ef-rli__t"></h3>
      <button type="button" class="ef-rli__btn">Replay</button>
    </div>
    <div class="ef-rli__layers" aria-hidden="true"></div>
  </div>
</div>

CSS

/* Enrichr Library · https://enrichr.dev */
.ef-rli { width: 100%; }
.ef-rli__stage { position: relative; isolation: isolate; overflow: hidden; width: 100%; min-height: 18rem; border-radius: 1rem; background: #0c1e18; color: #fbf8f1; display: flex; align-items: center; font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
.ef-rli__copy { position: relative; z-index: 1; box-sizing: border-box; width: 100%; padding: 1.6rem clamp(1.2rem, 5vw, 3rem); display: flex; flex-direction: column; align-items: flex-start; gap: 0.9rem; }
.ef-rli__eye { margin: 0; font-size: 0.72rem; letter-spacing: 0.2em; text-transform: uppercase; opacity: 0.65; }
.ef-rli__t { margin: 0; font-size: clamp(2rem, 8vw, 4.6rem); font-weight: 800; line-height: 0.98; letter-spacing: -0.03em; max-width: 12ch; text-wrap: balance; }
.ef-rli__btn { appearance: none; margin-top: 0.4rem; background: transparent; color: inherit; border: 1.5px solid currentColor; border-radius: 999px; font: inherit; font-size: 0.82rem; font-weight: 700; letter-spacing: 0.04em; padding: 0.55rem 1.2rem; cursor: pointer; transition: background-color 200ms, color 200ms; }
.ef-rli__btn:hover { background: #fbf8f1; color: #0c1e18; }
.ef-rli__btn:focus-visible { outline: 2px solid #e6ff55; outline-offset: 3px; }
.ef-rli__layers { position: absolute; inset: 0; z-index: 2; pointer-events: none; }
.ef-rli__layer { position: absolute; inset: 0; will-change: clip-path; }

JavaScript

// Enrichr Library · https://enrichr.dev
// Rapid layers intro: a stack of full-bleed colour layers wipes in one after another, then wipes back out in the same direction to reveal a headline.
// Built with clip-path and the Web Animations API. Plays when scrolled into view and again from the Replay button.
// Reduced motion shows the headline straight away and the button only does a soft fade.
// mount(root, { title, direction ("right", "left", "up" or "down"), layers (3-8), colorA, colorB })
function mount(root, o) {
  var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  var stage = root.querySelector(".ef-rli__stage");
  var holder = root.querySelector(".ef-rli__layers");
  var titleEl = root.querySelector(".ef-rli__t");
  var copy = root.querySelector(".ef-rli__copy");
  var eye = root.querySelector(".ef-rli__eye");
  var btn = root.querySelector(".ef-rli__btn");
  var opts = {}, layers = [], anims = [], io = null, played = false, timer = 0;
  var FULL = "inset(0% 0% 0% 0%)";
  var SHAPES = {
    right: ["inset(0% 100% 0% 0%)", "inset(0% 0% 0% 100%)"],
    left: ["inset(0% 0% 0% 100%)", "inset(0% 100% 0% 0%)"],
    down: ["inset(0% 0% 100% 0%)", "inset(100% 0% 0% 0%)"],
    up: ["inset(100% 0% 0% 0%)", "inset(0% 0% 100% 0%)"]
  };
  function shape() { return SHAPES[opts.direction] || SHAPES.right; }

  function clearAnims() { anims.forEach(function (a) { try { a.cancel(); } catch (e) {} }); anims = []; clearTimeout(timer); }
  function build() {
    clearAnims();
    holder.textContent = "";
    layers = [];
    var n = opts.layers;
    for (var i = 0; i < n; i++) {
      var d = document.createElement("div");
      d.className = "ef-rli__layer";
      var m = n > 1 ? Math.round((i / (n - 1)) * 100) : 100;
      var m2 = Math.min(100, m + 14);
      d.style.background = "linear-gradient(135deg, color-mix(in srgb, " + opts.colorB + " " + m + "%, " + opts.colorA + "), color-mix(in srgb, " + opts.colorB + " " + m2 + "%, " + opts.colorA + "))";
      holder.appendChild(d);
      layers.push(d);
    }
    rest();
  }
  function rest() {
    if (reduce) { holder.style.display = "none"; copy.style.opacity = "1"; return; }
    holder.style.display = "";
    layers.forEach(function (l) { l.style.clipPath = played ? shape()[1] : shape()[0]; });
    copy.style.opacity = played ? "1" : "0";
  }
  function play() {
    clearAnims();
    if (reduce) {
      copy.style.opacity = "1";
      if (copy.animate) anims.push(copy.animate([{ opacity: 0.2 }, { opacity: 1 }], { duration: 420, easing: "ease-out" }));
      return;
    }
    played = true;
    var s = shape(), n = layers.length;
    var dIn = 520, stag = Math.max(40, Math.round(420 / n)), hold = 120, dOut = 520;
    var inTotal = dIn + stag * (n - 1);
    var outStart = inTotal + hold;
    var ease = "cubic-bezier(.76,0,.2,1)";
    layers.forEach(function (l, i) {
      l.style.clipPath = s[0];
      anims.push(l.animate([{ clipPath: s[0] }, { clipPath: FULL }], { duration: dIn, delay: i * stag, easing: ease, fill: "both" }));
      anims.push(l.animate([{ clipPath: FULL }, { clipPath: s[1] }], { duration: dOut, delay: outStart + (n - 1 - i) * stag, easing: ease, fill: "forwards" }));
    });
    copy.style.opacity = "1";
    var ch = [eye, titleEl, btn];
    ch.forEach(function (el, i) {
      anims.push(el.animate([{ opacity: 0, transform: "translateY(36px)" }, { opacity: 1, transform: "translateY(0)" }], { duration: 700, delay: outStart + stag * (n - 1) * 0.6 + i * 110, easing: "cubic-bezier(.2,.8,.2,1)", fill: "both" }));
    });
  }
  function onBtn() { play(); }
  function update(next) {
    var prev = opts;
    opts = Object.assign({ title: "Make it move", direction: "right", layers: 5, colorA: "#005236", colorB: "#e6ff55" }, opts, next || {});
    opts.layers = Math.max(3, Math.min(8, Math.round(opts.layers)));
    titleEl.textContent = opts.title;
    var rebuild = prev.layers !== opts.layers || prev.colorA !== opts.colorA || prev.colorB !== opts.colorB;
    if (rebuild) build();
    if (prev.direction !== undefined && (rebuild || prev.direction !== opts.direction)) { played = false; play(); }
  }
  update(o);
  btn.addEventListener("click", onBtn);
  if ("IntersectionObserver" in window && !reduce) {
    io = new IntersectionObserver(function (en) { if (en[0].isIntersecting && !played) { play(); io.disconnect(); io = null; } }, { threshold: 0.35 });
    io.observe(stage);
  } else if (!reduce) { play(); }
  return { update: update, destroy: function () { clearAnims(); if (io) io.disconnect(); btn.removeEventListener("click", onBtn); } };
}

// Usage
var fx = mount(document.querySelector("#my-root"), {"title":"Make it move","direction":"right","layers":5,"colorA":"#005236","colorB":"#e6ff55"});

How it works

Each layer is a full-size block whose clip-path starts as an empty inset. The Web Animations API animates it to a full inset, with each layer delayed a little more than the one below, so the colours stack up. After a short hold the layers animate out to an empty inset on the far side, starting with the top layer, so the wipe keeps travelling in the same direction and reveals the headline underneath. Layer colours are mixed between the two colour params with color-mix. The text rises in as the last layers clear. The animation plays once when the element scrolls into view, and again from the Replay button or when you change the direction.

Accessibility and performance

The headline is real text and is present in the document from the start, only its opacity is animated. The layers are decorative and hidden from assistive technology, and they never receive pointer events. With reduced motion the headline is shown immediately with no wipe, and Replay only performs a soft fade. The Replay control is a native button with a visible focus ring.

Clip-path animations on a handful of full-size layers. For large screens keep the layer count at eight or fewer. Animations are cancelled when the element is destroyed or replayed.

SectionHero
TechniquesReveal and intro, Stagger
StackVanilla JS, CSS
Works withReact, Next.js, Vanilla
DependenciesNone
Statustested
Inspired byRapid Image Layers Animation 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.

Split text revealA headline whose words rise out of a mask one after another when it scrolls into view. Letter pop introEach letter overshoots its size and settles, staggered left to right, for a bold page intro. Circle reveal introA gate button opens a circle from the click point that grows to reveal the page.

FAQ

Can it run on page load instead of on scroll?

Yes. Call the replay on mount by removing the IntersectionObserver gate, or trigger it from your own load event.

Can the layers be images?

Yes. Replace the gradient backgrounds on the .ef-rli__layer elements with images or video frames. Keep them the same size as the stage.

How do I change the speed?

The durations are constants near the top of play(). Lower dIn and dOut for a snappier wipe or raise the hold value to pause on the full colour.