Copied!
Full page

Rotated reveal overlays

A page-transition style where rotated colour panels sweep across the stage to cover one scene, then carry on to reveal the next, driven by buttons, dots and arrow keys.

← Library

Element · Vanilla JS + CSS + Web Animations APIUpdated November 6, 2026

HeroElementVanilla JS + CSS + Web Animations API

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 "Rotated reveal overlays" for my site using the Enrichr Library element at https://enrichr.dev/elements/rotated-reveal-overlays.html.

Read the machine-readable spec and source at https://enrichr.dev/elements/rotated-reveal-overlays.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
- angle: 24
- panels: 5
- duration: 1100
- stagger: 70

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/rotated-reveal-overlays.json.

Code

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

HTML

<div class="ef-rro">
  <div class="ef-rro__stage" tabindex="0" role="group" aria-label="Scene transitions. Use the left and right arrow keys or the buttons.">
    <section class="ef-rro__scene ef-rro__scene--a is-active" data-i="0" aria-label="Scene 1 of 3">
      <svg class="ef-rro__art" viewBox="1513.7 1453.9 322.3 322.3" aria-hidden="true"><path fill="currentColor" 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"/></svg>
      <div class="ef-rro__txt"><span class="ef-rro__k">Scene 01</span><h3 class="ef-rro__t">Fold</h3><p class="ef-rro__p">Pieces meet at the edge.</p></div>
    </section>
    <section class="ef-rro__scene ef-rro__scene--b" data-i="1" aria-label="Scene 2 of 3">
      <svg class="ef-rro__art" viewBox="1423.36 860.8 503.07 322.3" aria-hidden="true"><path fill="currentColor" 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"/></svg>
      <div class="ef-rro__txt"><span class="ef-rro__k">Scene 02</span><h3 class="ef-rro__t">Signal</h3><p class="ef-rro__p">One bright joint in the dark.</p></div>
    </section>
    <section class="ef-rro__scene ef-rro__scene--c" data-i="2" aria-label="Scene 3 of 3">
      <svg class="ef-rro__art" viewBox="1513.7 166.86 322.3 503.06" aria-hidden="true"><path fill="currentColor" 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"/></svg>
      <div class="ef-rro__txt"><span class="ef-rro__k">Scene 03</span><h3 class="ef-rro__t">Canopy</h3><p class="ef-rro__p">Everything fits, finally.</p></div>
    </section>
    <div class="ef-rro__sweep" aria-hidden="true"></div>
    <div class="ef-rro__ui">
      <button type="button" class="ef-rro__btn" data-d="-1" aria-label="Previous scene">&#8249;</button>
      <div class="ef-rro__dots"></div>
      <button type="button" class="ef-rro__btn" data-d="1" aria-label="Next scene">&#8250;</button>
    </div>
  </div>
</div>

CSS

/* Enrichr Library · https://enrichr.dev */
.ef-rro { width: 100%; }
.ef-rro__stage { position: relative; box-sizing: border-box; width: 100%; height: 22rem; border-radius: 1rem; overflow: hidden; isolation: isolate; font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; background: #0c1e18; outline: none; }
.ef-rro__stage:focus-visible { box-shadow: inset 0 0 0 3px var(--ef-acc, #e6ff55); }
.ef-rro__scene { position: absolute; inset: 0; display: flex; align-items: flex-end; padding: 1.6rem 1.6rem 4.2rem; visibility: hidden; }
.ef-rro__scene.is-active { visibility: visible; }
.ef-rro__scene--a { background: #fbf8f1; color: #0c1e18; }
.ef-rro__scene--a .ef-rro__art { color: #005236; }
.ef-rro__scene--b { background: #0c1e18; color: #fbf8f1; }
.ef-rro__scene--b .ef-rro__art { color: #e6ff55; }
.ef-rro__scene--c { background: linear-gradient(150deg, #0a6b46, #005236); color: #fbf8f1; }
.ef-rro__scene--c .ef-rro__art { color: #c6f26b; }
.ef-rro__art { position: absolute; right: 8%; top: 12%; width: 46%; height: 62%; display: block; }
.ef-rro__txt { position: relative; max-width: 60%; }
.ef-rro__k { display: block; font-size: 0.68rem; font-weight: 800; letter-spacing: 0.16em; text-transform: uppercase; opacity: 0.75; }
.ef-rro__t { margin: 0.25rem 0 0; font-size: 2.2rem; font-weight: 800; letter-spacing: -0.03em; line-height: 1; }
.ef-rro__p { margin: 0.5rem 0 0; font-size: 0.9rem; opacity: 0.8; }
.ef-rro__sweep { position: absolute; left: -60%; top: -60%; width: 220%; height: 220%; display: flex; pointer-events: none; transform: rotate(var(--ef-rot, 24deg)); z-index: 2; }
.ef-rro__pan { flex: 1 1 0; margin-left: -1px; transform: translateY(102%); will-change: transform; }
.ef-rro__ui { position: absolute; left: 0.8rem; right: 0.8rem; bottom: 0.8rem; display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; z-index: 3; }
.ef-rro__btn { width: 2.4rem; height: 2.4rem; border-radius: 50%; border: 0; background: #0c1e18; color: #fbf8f1; box-shadow: 0 0 0 1px rgba(251, 248, 241, 0.25); font-size: 1.4rem; line-height: 1; cursor: pointer; display: grid; place-items: center; padding: 0 0 0.12rem; transition: background 200ms, color 200ms; }
.ef-rro__btn:hover { background: var(--ef-acc, #e6ff55); color: #0c1e18; }
.ef-rro__btn:focus-visible, .ef-rro__dot:focus-visible { outline: 2px solid var(--ef-acc, #e6ff55); outline-offset: 2px; }
.ef-rro__dots { display: flex; gap: 0.5rem; padding: 0.45rem 0.7rem; border-radius: 999px; background: #0c1e18; box-shadow: 0 0 0 1px rgba(251, 248, 241, 0.25); }
.ef-rro__dot { width: 0.6rem; height: 0.6rem; padding: 0; border: 0; border-radius: 50%; background: rgba(251, 248, 241, 0.45); cursor: pointer; transition: background 200ms, transform 200ms; }
.ef-rro__dot.is-on { background: var(--ef-acc, #e6ff55); transform: scale(1.3); }
@media (prefers-reduced-motion: reduce) { .ef-rro__btn, .ef-rro__dot { transition: none; } }

JavaScript

// Enrichr Library · https://enrichr.dev
// Rotated reveal overlays: a page-transition style. A set of rotated panels sweeps across the stage to cover the current scene, the scene
// underneath is swapped while it is fully hidden, then the panels carry on and reveal the next one. Buttons, dots and the arrow keys
// trigger it. Built with the Web Animations API on transforms only. Reduced motion swaps scenes instantly.
// An original rebuild from the general idea of rotated page-transition panels.
// mount(root, { accent, angle (deg), panels (2-8), duration (ms), stagger (ms) })
function mount(root, o) {
  var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  var stage = root.querySelector(".ef-rro__stage");
  var sweep = root.querySelector(".ef-rro__sweep");
  var dotsEl = root.querySelector(".ef-rro__dots");
  var scenes = Array.prototype.slice.call(root.querySelectorAll(".ef-rro__scene"));
  var opts = {}, offs = [], pans = [], dots = [], anims = [], cur = 0, busy = false, dead = false, gen = 0;

  function listen(el, ev, fn) { el.addEventListener(ev, fn); offs.push(function () { el.removeEventListener(ev, fn); }); }
  function colours() { return ["#005236", opts.accent, "#0a6b46", "#c6f26b", "#0c1e18", "#2f7d5b", "#fbf8f1", "#e6ff55"]; }
  function build() {
    sweep.textContent = "";
    pans = [];
    var c = colours();
    for (var i = 0; i < opts.panels; i++) {
      var p = document.createElement("div");
      p.className = "ef-rro__pan";
      p.style.background = c[i % c.length];
      sweep.appendChild(p);
      pans.push(p);
    }
  }
  function mark() {
    scenes.forEach(function (s, i) { s.classList.toggle("is-active", i === cur); });
    dots.forEach(function (d, i) { d.classList.toggle("is-on", i === cur); d.setAttribute("aria-current", i === cur ? "true" : "false"); });
  }
  function run(from, to, ease, delayStep, dur) {
    return Promise.all(pans.map(function (p, i) {
      var a = p.animate([{ transform: "translateY(" + from + ")" }, { transform: "translateY(" + to + ")" }], { duration: dur, delay: i * delayStep, easing: ease, fill: "forwards" });
      anims.push(a);
      return a.finished;
    }));
  }
  function clearAnims() { anims.forEach(function (a) { try { a.cancel(); } catch (e) {} }); anims = []; }
  function go(to) {
    to = (to + scenes.length) % scenes.length;
    if (busy || to === cur) return;
    var dir = (to > cur || (cur === scenes.length - 1 && to === 0)) ? 1 : -1;
    if (cur === 0 && to === scenes.length - 1) dir = -1;
    if (reduce || !pans[0] || !pans[0].animate) { cur = to; mark(); return; }
    busy = true;
    var my = ++gen;
    var half = opts.duration / 2;
    var inFrom = dir > 0 ? "102%" : "-102%", outTo = dir > 0 ? "-102%" : "102%";
    run(inFrom, "0%", "cubic-bezier(.7,0,.2,1)", opts.stagger, half).then(function () {
      if (dead || my !== gen) throw 0;
      cur = to; mark();
      return run("0%", outTo, "cubic-bezier(.7,0,.2,1)", opts.stagger, half);
    }).then(function () {
      if (dead || my !== gen) return;
      clearAnims(); busy = false;
    }).catch(function () { if (my === gen) { clearAnims(); busy = false; } });
  }
  function onKey(e) {
    if (e.key === "ArrowRight") { e.preventDefault(); go(cur + 1); }
    else if (e.key === "ArrowLeft") { e.preventDefault(); go(cur - 1); }
  }
  scenes.forEach(function (s, i) {
    var d = document.createElement("button");
    d.type = "button"; d.className = "ef-rro__dot";
    d.setAttribute("aria-label", "Go to scene " + (i + 1));
    listen(d, "click", function () { go(i); });
    dotsEl.appendChild(d); dots.push(d);
  });
  Array.prototype.forEach.call(root.querySelectorAll(".ef-rro__btn"), function (b) {
    listen(b, "click", function () { go(cur + Number(b.getAttribute("data-d"))); });
  });
  listen(stage, "keydown", onKey);

  function update(next) {
    var prev = opts;
    opts = Object.assign({ accent: "#e6ff55", angle: 24, panels: 5, duration: 1100, stagger: 70 }, opts, next || {});
    opts.panels = Math.max(2, Math.min(8, Math.round(opts.panels)));
    stage.style.setProperty("--ef-acc", opts.accent);
    stage.style.setProperty("--ef-rot", opts.angle + "deg");
    if (prev.panels !== opts.panels || prev.accent !== opts.accent) { if (busy) { gen++; clearAnims(); busy = false; } build(); }
    mark();
  }
  update(o);
  return {
    update: update,
    destroy: function () {
      dead = true; gen++; clearAnims();
      offs.forEach(function (f) { f(); }); offs = [];
    }
  };
}

// Usage
var fx = mount(document.querySelector("#my-root"), {"accent":"#e6ff55","angle":24,"panels":5,"duration":1100,"stagger":70});

How it works

A sweep layer larger than the stage holds a row of panels and is rotated by the angle setting, so the panels travel on a diagonal. To change scene the panels slide in from one side with a stagger until the stage is fully covered. Only then is the active scene swapped, and a second animation slides the panels out the other side to uncover the new scene. Both phases use the Web Animations API on transform, and the second phase starts when the first has finished, so the swap is never visible. Going back runs the same sweep in the opposite direction. Input is ignored while a transition is running.

Accessibility and performance

The stage is focusable and left and right arrow keys, the buttons and the dots all change scene. Only the active scene is visible to assistive tech because the others are hidden. Reduced motion skips the sweep and swaps scenes instantly. The panels are decorative and hidden from screen readers.

Only transforms animate and each panel is its own compositor layer for the length of the transition. With 8 panels this is light; the sweep layer is cleaned up once the animation ends.

SectionFull page
TechniquesReveal and intro, Stagger
StackVanilla JS, CSS, Web Animations API
Works withReact, Next.js, Vanilla
DependenciesNone
Statustested
Inspired byRotatedRevealers 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.

Rapid layers introA stack of full-bleed colour layers wipes in quickly one after another, then wipes away to reveal a headline, with a replay button. Island menuA round menu button that stretches into a wide pill, spins its logo in, turns its bars into a cross and pops a panel of links out of the corner. Staggered grid revealA grid of tiles that flip up one after another in random, reading, centre-out or edges-in order, then spin their glyphs into place.

FAQ

Can I use real pages instead of scenes?

Yes. Swap the content of the scene sections for your own, or run your router navigation at the moment the panels cover the stage, which is the point where the scene is swapped in the code.

How do I change the direction of the sweep?

Swap the translateY start and end values in go(). The first phase slides in from inFrom and the second slides out to outTo, so exchanging them reverses it.

Why do the panels overlap slightly?

Each panel overlaps its neighbour by one pixel so no hairline gap shows between them after the rotation.