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 "Editorial parallax showcase" for my site using the Enrichr Library element at https://enrichr.dev/elements/editorial-parallax-showcase.html.
Read the machine-readable spec and source at https://enrichr.dev/elements/editorial-parallax-showcase.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:
- depth: 1200
- photo: corgi-2
- eyebrow: How we build
- lede: Build pages that move with calm, deliberate care.
- heading: Clear, Calm, Considered
- word: motion.
- background: #fbf8f1
- ink: #0c1e18
- accent: #1a7a4c
- card: #ffffff
- highlight: #e6ff55
- warm: #e54c1e
- leaf: #005236
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/editorial-parallax-showcase.json.
Code
Mount it on an element that contains the markup. It returns an object with update(options) and destroy().
HTML
<div class="ef-eps">
<div class="ef-eps__intro">
<span class="ef-eps__eyebrow">How we build</span>
<h2 class="ef-eps__lede">Build pages that move with calm, deliberate care.</h2>
<p class="ef-eps__intro-text">We pair careful layout with motion that earns its place. Real text, light code, and nothing that gets in the reader's way, from the first line to the last.</p>
<div class="ef-eps__prompt">
<span class="ef-eps__prompt-text">Scroll to explore the parts</span>
<svg class="ef-eps__down" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 14l-7 7m0 0l-7-7m7 7V3"/></svg>
</div>
</div>
<div class="ef-eps__main">
<div class="ef-eps__head">
<div class="ef-eps__head-l">
<h2 class="ef-eps__heading">Clear, Calm, Considered<br><span class="ef-eps__serif">motion.</span></h2>
<p class="ef-eps__sub">Plain code that does what it says, kind to readers and to their batteries, with no exceptions.</p>
</div>
<div class="ef-eps__head-r" aria-hidden="true">
<svg class="ef-eps__curve" viewBox="0 0 443 454" fill="none" focusable="false"><path fill="currentColor" d="M359.477 453.019C359.488 453.324 359.743 453.562 360.048 453.551L365.011 453.376C365.315 453.365 365.554 453.11 365.543 452.805C365.532 452.501 365.277 452.262 364.972 452.273L360.56 452.429L360.405 448.017C360.394 447.712 360.138 447.474 359.834 447.485C359.529 447.496 359.291 447.751 359.302 448.056L359.477 453.019ZM1.13069 19.0358C201.979 -29.9272 338.641 28.0524 401.572 123.362C464.513 218.687 453.828 351.533 359.625 452.623L360.432 453.376C454.944 351.955 465.745 218.551 402.493 122.754C339.23 26.9416 202.021 -31.0734 0.869309 17.9636L1.13069 19.0358Z"/></svg>
</div>
</div>
<div class="ef-eps__layout">
<div class="ef-eps__pair">
<div class="ef-eps__card ef-eps__float ef-eps__c1" data-speed="0.35">
<div class="ef-eps__icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364-6.364l-.707.707M6.343 17.657l-.707.707m0-12.728l.707.707m12.728 12.728l.707-.707M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg></div>
<h3 class="ef-eps__card-title">Real Text, Always</h3>
<p class="ef-eps__card-text">Headings stay headings and links stay links, so screen readers and search engines read the page as it was written.</p>
</div>
<div class="ef-eps__card ef-eps__float ef-eps__c2" data-speed="0.12">
<div class="ef-eps__icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg></div>
<h3 class="ef-eps__card-title">Open Code</h3>
<p class="ef-eps__card-text">No black boxes. Every part ships as plain HTML, CSS and JavaScript you can read, copy and change.</p>
</div>
</div>
<div class="ef-eps__center">
<div class="ef-eps__frame">
<div class="ef-eps__float ef-eps__shape" data-speed="0.04">
<div class="ef-eps__oblong"><img class="ef-eps__img" src="/img/library/corgi-2.jpg" alt="A corgi smiling in a field of flowers" decoding="async"></div>
</div>
<div class="ef-eps__float ef-eps__spiral" data-speed="-0.15" aria-hidden="true">
<svg viewBox="0 0 100 100" fill="none" focusable="false"><path d="M20,50 C20,30 40,15 60,20 C75,25 85,45 80,65 C75,80 50,90 35,80 C25,72 30,55 45,50 C55,45 65,55 60,65 C55,72 45,70 42,62" stroke="currentColor" stroke-width="4" stroke-linecap="round" fill="none"/></svg>
</div>
<div class="ef-eps__float ef-eps__leaf" data-speed="0.25" aria-hidden="true">
<svg viewBox="0 0 100 100" fill="none" focusable="false"><path d="M10,80 Q35,85 55,65 Q85,35 90,10 Q65,15 35,45 Q15,65 10,80 Z" fill="currentColor" fill-opacity="0.15" stroke="currentColor" stroke-width="2"/><path d="M10,80 Q50,55 90,10" stroke="currentColor" stroke-width="2"/></svg>
</div>
</div>
</div>
<div class="ef-eps__pair">
<div class="ef-eps__card ef-eps__float ef-eps__c3" data-speed="0.18">
<div class="ef-eps__icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg></div>
<h3 class="ef-eps__card-title">Light on Its Feet</h3>
<p class="ef-eps__card-text">Motion runs on the compositor, pauses off screen and respects reduced motion, so pages stay quick on any phone.</p>
</div>
<div class="ef-eps__card ef-eps__float ef-eps__c4" data-speed="0.08">
<div class="ef-eps__icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/></svg></div>
<h3 class="ef-eps__card-title">Built to Last</h3>
<p class="ef-eps__card-text">No frameworks to update and no libraries to load. What you paste today still works next year.</p>
</div>
</div>
</div>
</div>
</div>CSS
/* Enrichr Library · https://enrichr.dev */
.ef-eps { --ef-eps-bg: #fbf8f1; --ef-eps-ink: #0c1e18; --ef-eps-acc: #1a7a4c; --ef-eps-card: #ffffff; --ef-eps-hi: #e6ff55; --ef-eps-warm: #e54c1e; --ef-eps-leaf: #005236;
--ef-eps-line: color-mix(in srgb, var(--ef-eps-ink) 10%, var(--ef-eps-bg)); --ef-eps-muted: color-mix(in srgb, var(--ef-eps-ink) 62%, var(--ef-eps-bg));
--ef-eps-serif: "Instrument Serif", "Playfair Display", Georgia, "Times New Roman", serif;
position: relative; width: 100%; box-sizing: border-box; overflow: hidden; border-radius: 1rem; background: var(--ef-eps-bg); color: var(--ef-eps-ink); font-family: "Instrument Sans", system-ui, sans-serif; -webkit-font-smoothing: antialiased; container-type: inline-size; }
.ef-eps *, .ef-eps *::before, .ef-eps *::after { box-sizing: border-box; }
.ef-eps svg { display: block; }
/* intro */
.ef-eps__intro { max-width: 80rem; margin: 0 auto; padding: 96px 24px 32px; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; }
.ef-eps__eyebrow { display: block; margin-bottom: 16px; font-size: 12px; font-weight: 700; letter-spacing: 0.3em; text-transform: uppercase; color: var(--ef-eps-acc); }
.ef-eps .ef-eps__lede { max-width: 56rem; margin: 0 0 24px; font-family: var(--ef-eps-serif); font-style: italic; font-weight: 400; font-size: 36px; line-height: 1.15; letter-spacing: normal; text-transform: none; color: var(--ef-eps-ink); opacity: 1; }
.ef-eps .ef-eps__intro-text { max-width: 36rem; margin: 0 0 32px; font-size: 14px; line-height: 1.625; color: var(--ef-eps-muted); opacity: 1; }
.ef-eps__prompt { display: flex; flex-direction: column; align-items: center; gap: 12px; }
.ef-eps__prompt-text { font-size: 12px; font-weight: 700; letter-spacing: 0.25em; text-transform: uppercase; color: color-mix(in srgb, var(--ef-eps-ink) 40%, transparent); animation: ef-eps-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
.ef-eps__down { width: 20px; height: 20px; color: var(--ef-eps-acc); animation: ef-eps-bounce 1s infinite; }
@keyframes ef-eps-pulse { 50% { opacity: 0.5; } }
@keyframes ef-eps-bounce { 0%, 100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(0.8, 0, 1, 1); } 50% { transform: none; animation-timing-function: cubic-bezier(0, 0, 0.2, 1); } }
.ef-eps.is-off .ef-eps__prompt-text, .ef-eps.is-off .ef-eps__down { animation-play-state: paused; }
/* main section */
.ef-eps__main { position: relative; max-width: 1440px; margin: 0 auto; padding: 96px 24px 8rem; user-select: none; }
.ef-eps__head { position: relative; z-index: 10; display: grid; grid-template-columns: 1fr; gap: 32px; margin-bottom: 64px; }
.ef-eps__head-l { display: flex; flex-direction: column; justify-content: center; }
.ef-eps__head-r { display: none; }
.ef-eps .ef-eps__heading { margin: 0; font-family: "Instrument Sans", system-ui, sans-serif; font-size: 30px; font-weight: 400; line-height: 1.25; letter-spacing: -0.025em; text-transform: none; color: var(--ef-eps-ink); opacity: 1; }
.ef-eps__serif { position: relative; display: inline-block; margin-top: 8px; font-family: var(--ef-eps-serif); font-style: italic; }
.ef-eps__serif::after { content: ""; position: absolute; bottom: 4px; left: 0; width: 100%; height: 1px; background: var(--ef-eps-ink); transform: scaleX(0); transform-origin: right; transition: transform 0.8s cubic-bezier(0.77, 0, 0.175, 1); }
.ef-eps__serif.is-active::after { transform: scaleX(1); transform-origin: left; }
.ef-eps .ef-eps__sub { max-width: 28rem; margin: 16px 0 0; font-size: 14px; line-height: 1.5; color: var(--ef-eps-muted); opacity: 1; }
.ef-eps__curve { width: 280px; height: 280px; transform: rotate(15deg); color: color-mix(in srgb, var(--ef-eps-acc) 60%, transparent); }
.ef-eps__layout { position: relative; width: 100%; min-height: 1000px; display: flex; flex-direction: column; }
.ef-eps__pair { display: flex; flex-direction: column; gap: 32px; justify-content: space-between; padding: 0 8px; }
.ef-eps__float { will-change: transform; transition: transform 0.15s cubic-bezier(0.25, 1, 0.5, 1); }
.ef-eps__card { width: 100%; max-width: 320px; padding: 2.5rem 2rem; border-radius: 1.5rem; background: color-mix(in srgb, var(--ef-eps-card) 94%, transparent); -webkit-backdrop-filter: blur(20px); backdrop-filter: blur(20px); border: 1px solid color-mix(in srgb, var(--ef-eps-line) 40%, transparent); box-shadow: 0 20px 50px rgba(0, 0, 0, 0.02), inset 0 0 0 1px color-mix(in srgb, var(--ef-eps-card) 90%, transparent); transition: all 0.4s cubic-bezier(0.25, 1, 0.5, 1); }
.ef-eps__card:hover { transform: translateY(-5px); box-shadow: 0 30px 60px color-mix(in srgb, var(--ef-eps-acc) 12%, transparent), inset 0 0 0 1px var(--ef-eps-card); border-color: var(--ef-eps-acc); }
.ef-eps__icon { display: flex; align-items: center; justify-content: center; width: 52px; height: 52px; margin-bottom: 1.5rem; border-radius: 50%; background: var(--ef-eps-bg); border: 1px solid color-mix(in srgb, var(--ef-eps-line) 60%, transparent); box-shadow: 0 6px 15px rgba(0, 0, 0, 0.02); color: var(--ef-eps-ink); transition: all 0.3s ease; }
.ef-eps__icon svg { width: 20px; height: 20px; }
.ef-eps__c1 .ef-eps__icon svg { width: 24px; height: 24px; }
.ef-eps__card:hover .ef-eps__icon { background: var(--ef-eps-ink); color: var(--ef-eps-hi); border-color: var(--ef-eps-ink); }
.ef-eps .ef-eps__card-title { margin: 0 0 8px; font-family: "Instrument Sans", system-ui, sans-serif; font-size: 18px; font-weight: 600; line-height: 1.55; letter-spacing: -0.025em; text-transform: none; color: var(--ef-eps-ink); opacity: 1; }
.ef-eps .ef-eps__card-text { margin: 0; font-size: 12px; line-height: 1.625; color: var(--ef-eps-muted); opacity: 1; }
.ef-eps__center { position: relative; z-index: 10; display: flex; justify-content: center; align-items: center; width: 290px; height: 390px; margin: 48px auto; }
.ef-eps__frame { position: relative; width: 100%; height: 100%; }
.ef-eps__shape { width: 100%; height: 100%; }
.ef-eps__oblong { position: relative; width: 100%; height: 100%; overflow: hidden; border-radius: 50% 50% 50% 50% / 45% 45% 55% 55%; transform: rotate(-12deg); box-shadow: 0 35px 80px color-mix(in srgb, var(--ef-eps-ink) 8%, transparent); border: 1px solid color-mix(in srgb, var(--ef-eps-line) 50%, transparent); background: var(--ef-eps-line); transition: transform 0.5s cubic-bezier(0.25, 1, 0.5, 1); }
.ef-eps__img { display: block; width: 100%; height: 115%; object-fit: cover; transform: rotate(12deg) scale(1.15) translateY(-5%); transition: transform 0.3s ease-out; }
.ef-eps__spiral { position: absolute; left: -48px; bottom: -32px; z-index: 30; width: 96px; height: 96px; color: color-mix(in srgb, var(--ef-eps-warm) 80%, transparent); filter: drop-shadow(0 10px 8px rgba(0, 0, 0, 0.04)) drop-shadow(0 4px 3px rgba(0, 0, 0, 0.1)); }
.ef-eps__leaf { position: absolute; right: -32px; top: -48px; z-index: 30; width: 80px; height: 80px; color: color-mix(in srgb, var(--ef-eps-leaf) 80%, transparent); filter: drop-shadow(0 4px 3px rgba(0, 0, 0, 0.07)) drop-shadow(0 2px 2px rgba(0, 0, 0, 0.06)); }
.ef-eps__spiral svg, .ef-eps__leaf svg { width: 100%; height: 100%; }
@container (min-width: 640px) {
.ef-eps__pair { flex-direction: row; padding: 0; }
}
@container (min-width: 768px) {
.ef-eps .ef-eps__lede { font-size: 60px; }
.ef-eps .ef-eps__intro-text { font-size: 16px; }
.ef-eps__main { padding-left: 48px; padding-right: 48px; }
.ef-eps .ef-eps__heading { font-size: 48px; }
.ef-eps .ef-eps__sub { font-size: 16px; }
.ef-eps__center { width: 350px; height: 480px; }
.ef-eps__spiral { width: 128px; height: 128px; }
.ef-eps__leaf { width: 112px; height: 112px; }
}
@container (min-width: 900px) {
.ef-eps__head { grid-template-columns: repeat(12, minmax(0, 1fr)); margin-bottom: 96px; }
.ef-eps__head-l { grid-column: span 8 / span 8; }
.ef-eps__head-r { grid-column: span 4 / span 4; display: flex; align-items: center; justify-content: flex-end; position: relative; }
.ef-eps .ef-eps__heading { font-size: 60px; }
.ef-eps__layout { display: block; }
.ef-eps__pair { display: contents; }
.ef-eps__card { position: absolute; width: 23%; max-width: 300px; }
.ef-eps__c1 { left: 2%; top: 12%; }
.ef-eps__c2 { left: 27%; top: 2%; z-index: 20; }
.ef-eps__c3 { right: 27%; bottom: 2%; z-index: 20; }
.ef-eps__c4 { right: 2%; bottom: 12%; }
.ef-eps__center { position: absolute; left: 50%; top: 8%; width: 40%; max-width: 480px; height: 480px; margin: 0; transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
.ef-eps__prompt-text, .ef-eps__down { animation: none; }
.ef-eps__float { transform: none !important; transition: none; }
.ef-eps__serif::after { transition: none; }
}JavaScript
// Enrichr Library · https://enrichr.dev
// Editorial parallax showcase: an editorial intro with a serif italic headline, then a section where four glass cards, a
// tilted capsule photo and two drawn accents overlap. As the section passes through the viewport each piece moves at its
// own speed: progress runs from 0 (section top at the bottom of the screen) to 1 (section bottom past the top), and each
// piece shifts by (progress - 0.5) x its speed x 1200 px, smoothed by a short CSS transition. The serif word gets an
// underline that draws in once the section is on screen. Below 900 px wide the layout stacks and the parallax switches off.
// Scroll work is one requestAnimationFrame per scroll event while the section is near the viewport. Reduced motion keeps
// everything in place, shows the underline at once and stops the scroll prompt from pulsing.
// mount(root, { depth (px of travel at speed 1), photo (corgi-1 to corgi-6), eyebrow, lede, heading, word,
// background, ink, accent, card, highlight, warm (spiral), leaf })
function mount(root, o) {
var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
var box = root.querySelector(".ef-eps");
var main = root.querySelector(".ef-eps__main");
var serif = root.querySelector(".ef-eps__serif");
var img = root.querySelector(".ef-eps__img");
var floats = [].slice.call(root.querySelectorAll(".ef-eps__float")).map(function (el) { return { el: el, speed: parseFloat(el.getAttribute("data-speed")) || 0 }; });
var heading = root.querySelector(".ef-eps__heading");
var opts = {}, raf = 0, near = true, io = null, io2 = null, ro = null, destroyed = false, wide = true;
var ALT = { "corgi-1": "A corgi puppy against an orange wall", "corgi-2": "A corgi smiling in a field of flowers", "corgi-3": "A corgi standing on a rock outdoors", "corgi-4": "Two dogs running down a dirt road", "corgi-5": "A corgi lying on a white background", "corgi-6": "A corgi standing on a white background" };
function num(v, d) { v = Number(v); return isFinite(v) ? v : d; }
function draw() {
raf = 0;
if (destroyed) return;
wide = box.clientWidth >= 900;
if (reduce || !wide) { floats.forEach(function (f) { f.el.style.transform = ""; }); return; }
if (!near) return;
var vh = window.innerHeight || document.documentElement.clientHeight || 800;
var r = main.getBoundingClientRect();
var p = Math.max(0, Math.min(1, (vh - r.top) / (vh + r.height)));
var c = p - 0.5;
floats.forEach(function (f) { f.el.style.transform = "translate3d(0," + (c * f.speed * opts.depth).toFixed(2) + "px,0)"; });
}
function kick() { if (!raf && !destroyed) raf = requestAnimationFrame(draw); }
function update(next) {
opts = Object.assign({
depth: 1200, photo: "corgi-2", eyebrow: "How we build", lede: "Build pages that move with calm, deliberate care.",
heading: "Clear, Calm, Considered", word: "motion.",
background: "#fbf8f1", ink: "#0c1e18", accent: "#1a7a4c", card: "#ffffff", highlight: "#e6ff55", warm: "#e54c1e", leaf: "#005236"
}, opts, next || {});
opts.depth = Math.max(0, num(opts.depth, 1200));
var photo = /^corgi-[1-6]$/.test(opts.photo) ? opts.photo : "corgi-2";
var src = "/img/library/" + photo + ".jpg";
if (img.getAttribute("src") !== src) { img.setAttribute("src", src); img.alt = ALT[photo]; }
root.querySelector(".ef-eps__eyebrow").textContent = opts.eyebrow;
root.querySelector(".ef-eps__lede").textContent = opts.lede;
heading.firstChild.nodeValue = opts.heading;
serif.textContent = opts.word;
var map = { background: "bg", ink: "ink", accent: "acc", card: "card", highlight: "hi", warm: "warm", leaf: "leaf" };
Object.keys(map).forEach(function (k) { box.style.setProperty("--ef-eps-" + map[k], opts[k]); });
kick();
}
update(o);
if (reduce) serif.classList.add("is-active");
window.addEventListener("scroll", kick, { passive: true, capture: true });
window.addEventListener("resize", kick, { passive: true });
if ("IntersectionObserver" in window) {
io = new IntersectionObserver(function (e) { near = e[0].isIntersecting; if (near) kick(); }, { rootMargin: "200px 0px" });
io.observe(main);
io2 = new IntersectionObserver(function (e) {
e.forEach(function (x) { if (x.isIntersecting) serif.classList.add("is-active"); });
}, { threshold: 0.15 });
io2.observe(main);
} else serif.classList.add("is-active");
var introIo = null;
if ("IntersectionObserver" in window) { introIo = new IntersectionObserver(function (e) { box.classList.toggle("is-off", !e[0].isIntersecting); }); introIo.observe(root.querySelector(".ef-eps__intro")); }
if ("ResizeObserver" in window) { ro = new ResizeObserver(kick); ro.observe(box); }
return {
update: update,
destroy: function () {
destroyed = true; cancelAnimationFrame(raf);
window.removeEventListener("scroll", kick, { passive: true, capture: true });
window.removeEventListener("resize", kick, { passive: true });
if (io) io.disconnect(); if (io2) io2.disconnect(); if (introIo) introIo.disconnect(); if (ro) ro.disconnect();
}
};
}
// Usage
var fx = mount(document.querySelector("#my-root"), {"depth":1200,"photo":"corgi-2","eyebrow":"How we build","lede":"Build pages that move with calm, deliberate care.","heading":"Clear, Calm, Considered","word":"motion.","background":"#fbf8f1","ink":"#0c1e18","accent":"#1a7a4c","card":"#ffffff","highlight":"#e6ff55","warm":"#e54c1e","leaf":"#005236"});How it works
The section's scroll progress runs from 0, when its top reaches the bottom of the screen, to 1, when its bottom passes the top: (screen height minus its top) divided by (screen height plus its height). Every floating piece has its own speed, from -0.15 for the spiral to 0.35 for the far left card, and moves down by (progress minus 0.5) times its speed times 1200 px, so all of them line up at the midpoint and spread apart before and after it. A short CSS transition on transform (0.4 seconds for the cards, 0.15 seconds for the photo and accents, on a soft ease-out curve) smooths the steps between scroll events. Scroll events from the page or any scrolling parent schedule one frame. The capsule is an ellipse with a taller bottom, turned -12 degrees, and the photo inside is turned back and enlarged so the dog stays upright. The serif word's underline draws from left to right over 0.8 seconds once the section is 15% on screen. Below 900 px wide the cards and photo stack and the parallax is off.
Accessibility and performance
Headings, card titles and text are real text in reading order, and the photo has alt text; the curve, spiral and leaf are hidden from screen readers. With prefers-reduced-motion nothing moves with the scroll, the underline is shown at once and the scroll prompt stops pulsing. Nothing needs a pointer; card hover is a visual extra.
Only transform changes on scroll, one translate per piece, with one getBoundingClientRect read per frame. Work happens only when the page scrolls and the section is near the screen. The cards' backdrop blur is the heaviest part; drop it if you target very old phones.
| Section | Full page |
|---|---|
| Techniques | Parallax and depth, Scroll-driven, Reveal and intro |
| Stack | Vanilla JS, CSS, SVG |
| Works with | React, Next.js, Vanilla |
| Dependencies | None |
| Status | tested |
| On CodePen | Open the live pen on our CodePen account; fork it to experiment. |
| Inspired by | Editorial parallax showcase study by Enrichr on CodePen. 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
How do I make the parallax stronger or calmer?
Change Parallax depth. Each piece travels its speed times the depth over the full scroll, so 600 halves every movement and 2400 doubles it. The individual speeds are the data-speed attributes in the markup.
Why does it stop moving on narrow screens?
Below 900 px the overlapping layout no longer fits, so the cards and photo stack in a column and stay put, as in the original. Overlapping cards moving over each other on a phone would cover the text.
Which font is the serif?
It asks for Instrument Serif or Playfair Display if your site loads one, and falls back to Georgia. Load any italic serif you like and put it first in --ef-eps-serif.
Where is the photo from?
Photos: Alvan Nee and fatty corgi on Unsplash. Pick another with the Capsule photo setting, or replace the img src in the markup with your own portrait photo.