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 "Continuous section slides" for my site using the Enrichr Library element at https://enrichr.dev/elements/continuous-section-slides.html.
Read the machine-readable spec and source at https://enrichr.dev/elements/continuous-section-slides.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
- sections: 4
- duration: 900
- parallax: 0.5
- sensitivity: 40
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/continuous-section-slides.json.
Code
Mount it on an element that contains the markup. It returns an object with update(options) and destroy().
HTML
<div class="ef-css">
<div class="ef-css__wrap" tabindex="0" role="group" aria-roledescription="slides" aria-label="Sections. Scroll, swipe or use the arrow keys to change section.">
<div class="ef-css__stage"></div>
<div class="ef-css__ui">
<span class="ef-css__cnt" aria-live="polite"></span>
<span class="ef-css__dots"></span>
</div>
<span class="ef-css__bar" aria-hidden="true"><span class="ef-css__fill"></span></span>
<span class="ef-css__hint" aria-hidden="true">Scroll, swipe or use arrow keys</span>
</div>
</div>CSS
/* Enrichr Library · https://enrichr.dev */
.ef-css { width: 100%; }
.ef-css__wrap { position: relative; box-sizing: border-box; width: 100%; height: 26rem; border-radius: 1rem; overflow: hidden; touch-action: pan-x; outline: none; color: #fbf8f1; font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
.ef-css__wrap:focus-visible { box-shadow: 0 0 0 2px var(--ef-acc, #e6ff55) inset; }
.ef-css__stage { position: absolute; inset: 0; }
.ef-css__sec { position: absolute; inset: 0; visibility: hidden; z-index: 1; will-change: clip-path; }
.ef-css__sec.is-cur { visibility: visible; z-index: 2; }
.ef-css__sec.is-in { visibility: visible; z-index: 3; }
.ef-css__bg { position: absolute; inset: 0; }
.ef-css__bg::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, rgba(0, 0, 0, 0.1), transparent 35%, rgba(0, 0, 0, 0.45)); }
.ef-css__sec.is-lite .ef-css__bg::after { background: linear-gradient(180deg, transparent, transparent 60%, rgba(0, 30, 24, 0.12)); }
.ef-css__in { position: absolute; inset: 0; display: flex; flex-direction: column; justify-content: center; padding: 2rem 4.2rem 2rem 2rem; will-change: transform; }
.ef-css__num { font-size: 0.75rem; font-weight: 700; letter-spacing: 0.18em; opacity: 0.8; }
.ef-css__t { margin: 0.5rem 0 0; font-size: clamp(2rem, 6vw, 3.6rem); font-weight: 800; letter-spacing: -0.03em; line-height: 1; }
.ef-css__p { margin: 0.8rem 0 0; max-width: 26rem; font-size: 0.95rem; line-height: 1.5; opacity: 0.85; }
.ef-css__ui { position: absolute; right: 1rem; top: 50%; transform: translateY(-50%); z-index: 5; display: flex; flex-direction: column; align-items: center; gap: 0.7rem; }
.ef-css__cnt { font-size: 0.68rem; font-weight: 700; letter-spacing: 0.1em; font-variant-numeric: tabular-nums; padding: 0.2rem 0.45rem; border-radius: 99px; background: rgba(12, 30, 24, 0.7); color: #fbf8f1; }
.ef-css__dots { display: flex; flex-direction: column; gap: 0.15rem; }
.ef-css__dot { box-sizing: border-box; width: 1.5rem; height: 1.5rem; padding: 0; border: 0; background: transparent; cursor: pointer; display: grid; place-items: center; border-radius: 50%; outline: none; }
.ef-css__dot::before { content: ""; width: 0.5rem; height: 0.5rem; border-radius: 50%; background: rgba(251, 248, 241, 0.9); box-shadow: 0 0 0 1.5px rgba(12, 30, 24, 0.6); transform: scale(0.7); opacity: 0.6; transition: transform 300ms, background 300ms, opacity 300ms; }
.ef-css__dot[aria-current="true"]::before { background: var(--ef-acc, #e6ff55); transform: scale(1.25); opacity: 1; }
.ef-css__dot:focus-visible { box-shadow: 0 0 0 2px var(--ef-acc, #e6ff55); }
.ef-css__bar { position: absolute; left: 0; right: 0; bottom: 0; height: 4px; z-index: 5; background: rgba(12, 30, 24, 0.35); }
.ef-css__fill { position: absolute; left: 0; top: 0; bottom: 0; width: 100%; background: var(--ef-acc, #e6ff55); transform-origin: left center; transform: scaleX(0); transition: transform 500ms cubic-bezier(.77, 0, .18, 1); }
.ef-css__hint { position: absolute; left: 2rem; bottom: 1rem; z-index: 5; font-size: 0.68rem; letter-spacing: 0.08em; padding: 0.2rem 0.55rem; border-radius: 99px; background: rgba(12, 30, 24, 0.7); color: #fbf8f1; pointer-events: none; }
.ef-css__wrap.is-rm .ef-css__fill, .ef-css__wrap.is-rm .ef-css__dot::before { transition: none; }
@media (prefers-reduced-motion: reduce) { .ef-css__fill, .ef-css__dot::before { transition: none; } }JavaScript
// Enrichr Library · https://enrichr.dev
// Continuous section slides: full-height sections that change one at a time on wheel, swipe or arrow keys (an observer-style input
// layer with a lock while a transition runs). The incoming section is revealed with a clip-path wipe while the content of both
// sections slides for a parallax feel. A fill bar, counter and dot buttons show progress. The wheel is captured only while the pointer
// is over the element, and not at the first or last section, so the page can still scroll past. Reduced motion swaps sections instantly.
// mount(root, { background, accent, sections (3-6), duration (ms), parallax (0-1), sensitivity (wheel px to trigger) })
function mount(root, o) {
var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
var wrap = root.querySelector(".ef-css__wrap");
var stage = root.querySelector(".ef-css__stage");
var dotsEl = root.querySelector(".ef-css__dots");
var cnt = root.querySelector(".ef-css__cnt");
var fill = root.querySelector(".ef-css__fill");
var hint = root.querySelector(".ef-css__hint");
var opts = {}, secs = [], dots = [], cur = 0, busy = false, acc = 0, lockUntil = 0, startY = null;
var timer = 0, accT = 0, hintT = 0, offs = [];
var DATA = [["Arrive", "A quiet start. The first section sets the tone before anything moves."], ["Gather", "Ideas land one at a time instead of all at once."], ["Build", "Each wipe hands the stage to the next section."], ["Refine", "Small passes, one section per gesture."], ["Ship", "A clean finish with the whole path behind it."], ["Repeat", "Go back up and it plays in reverse."]];
function bgFor(i, a) {
var d = "#0c1e18";
var p = i % 6;
if (p === 0) return ["radial-gradient(circle at 78% 28%, " + a + " 0 14%, transparent 15%), linear-gradient(160deg, #005236, " + d + ")", false];
if (p === 1) return ["linear-gradient(" + a + ", " + a + ")", true];
if (p === 2) return ["repeating-linear-gradient(120deg, #005236 0 16px, " + d + " 16px 32px)", false];
if (p === 3) return ["conic-gradient(from 210deg at 70% 60%, #005236, " + a + ", #fbf8f1, #005236)", false];
if (p === 4) return ["radial-gradient(circle at 25% 70%, " + a + " 0 18%, transparent 19%), linear-gradient(200deg, #0a6b46, " + d + ")", false];
return ["radial-gradient(circle, " + d + " 0 2.5px, transparent 3px) 0 0 / 18px 18px, linear-gradient(145deg, " + a + ", #bfe03a)", true];
}
function clearTimers() { clearTimeout(timer); clearTimeout(accT); clearTimeout(hintT); timer = accT = hintT = 0; }
function clear() { clearTimers(); busy = false; acc = 0; offs.forEach(function (f) { f(); }); offs = []; }
function show(i) {
secs.forEach(function (s, k) { s.setAttribute("aria-hidden", k === i ? "false" : "true"); });
dots.forEach(function (d, k) { if (k === i) d.setAttribute("aria-current", "true"); else d.removeAttribute("aria-current"); });
cnt.textContent = String(i + 1).padStart(2, "0") + " / " + String(secs.length).padStart(2, "0");
fill.style.transform = "scaleX(" + ((i + 1) / secs.length).toFixed(4) + ")";
}
function reset(s) {
s.classList.remove("is-cur", "is-in");
s.style.clipPath = ""; s.style.transition = "";
var inner = s.firstChild.nextSibling;
inner.style.transform = ""; inner.style.transition = "";
}
function go(n) {
if (busy || n === cur || n < 0 || n >= secs.length) return false;
var out = secs[cur], inn = secs[n], dir = n > cur ? 1 : -1;
var oi = out.firstChild.nextSibling, ii = inn.firstChild.nextSibling;
var d = reduce ? 0 : opts.duration;
var prev = cur;
cur = n;
show(n);
wrap.classList.add("is-used");
if (hint) hint.style.display = "none";
if (d === 0) {
reset(out); reset(inn); inn.classList.add("is-cur");
lockUntil = performance.now() + 120;
return true;
}
busy = true;
var shift = opts.parallax * 35;
inn.style.transition = "none"; ii.style.transition = "none";
inn.style.clipPath = dir > 0 ? "inset(100% 0% 0% 0%)" : "inset(0% 0% 100% 0%)";
ii.style.transform = "translateY(" + (dir * shift) + "%)";
inn.classList.add("is-in");
void inn.offsetWidth;
var tr = d + "ms cubic-bezier(.77,0,.18,1)";
inn.style.transition = "clip-path " + tr; ii.style.transition = "transform " + tr; oi.style.transition = "transform " + tr;
inn.style.clipPath = "inset(0% 0% 0% 0%)";
ii.style.transform = "translateY(0%)";
oi.style.transform = "translateY(" + (-dir * shift) + "%)";
timer = setTimeout(function () {
timer = 0;
reset(secs[prev]); reset(inn); inn.classList.add("is-cur");
busy = false;
lockUntil = performance.now() + 220;
}, d + 40);
return true;
}
function build() {
clear();
stage.textContent = ""; dotsEl.textContent = "";
secs = []; dots = [];
cur = 0;
for (var i = 0; i < opts.sections; i++) (function (i) {
var s = document.createElement("div");
s.className = "ef-css__sec";
var b = bgFor(i, opts.accent);
var bg = document.createElement("span"); bg.className = "ef-css__bg"; bg.style.background = b[0];
var inner = document.createElement("div"); inner.className = "ef-css__in";
var nm = document.createElement("span"); nm.className = "ef-css__num"; nm.textContent = "SECTION " + String(i + 1).padStart(2, "0");
var t = document.createElement("h3"); t.className = "ef-css__t"; t.textContent = DATA[i % DATA.length][0];
var p = document.createElement("p"); p.className = "ef-css__p"; p.textContent = DATA[i % DATA.length][1];
inner.appendChild(nm); inner.appendChild(t); inner.appendChild(p);
if (b[1]) { s.classList.add("is-lite"); s.style.color = "#0c1e18"; }
s.appendChild(bg); s.appendChild(inner);
stage.appendChild(s); secs.push(s);
var d = document.createElement("button");
d.type = "button"; d.className = "ef-css__dot"; d.setAttribute("aria-label", "Go to section " + (i + 1));
function ck() { go(i); }
d.addEventListener("click", ck);
offs.push(function () { d.removeEventListener("click", ck); });
dotsEl.appendChild(d); dots.push(d);
})(i);
secs[0].classList.add("is-cur");
show(0);
function wheel(e) {
if (!e.deltaY) return;
var dir = e.deltaY > 0 ? 1 : -1, next = cur + dir;
if (next < 0 || next >= secs.length) return;
e.preventDefault();
if (busy || performance.now() < lockUntil) { acc = 0; return; }
acc += e.deltaY;
clearTimeout(accT);
accT = setTimeout(function () { acc = 0; }, 160);
if (Math.abs(acc) >= opts.sensitivity) { acc = 0; go(next); }
}
function pd(e) { if (e.pointerType === "mouse") return; startY = e.clientY; }
function pu(e) {
if (startY === null) return;
var dy = startY - e.clientY; startY = null;
if (Math.abs(dy) >= 40) go(cur + (dy > 0 ? 1 : -1));
}
function pc() { startY = null; }
function key(e) {
var k = e.key, to = -1;
if (k === "ArrowDown" || k === "PageDown" || k === "ArrowRight") to = cur + 1;
else if (k === "ArrowUp" || k === "PageUp" || k === "ArrowLeft") to = cur - 1;
else if (k === "Home") to = 0;
else if (k === "End") to = secs.length - 1;
if (to < 0 || to >= secs.length) return;
e.preventDefault();
go(to);
}
wrap.addEventListener("wheel", wheel, { passive: false });
wrap.addEventListener("pointerdown", pd);
wrap.addEventListener("pointerup", pu);
wrap.addEventListener("pointercancel", pc);
wrap.addEventListener("keydown", key);
offs.push(function () {
wrap.removeEventListener("wheel", wheel, { passive: false });
wrap.removeEventListener("pointerdown", pd); wrap.removeEventListener("pointerup", pu);
wrap.removeEventListener("pointercancel", pc); wrap.removeEventListener("keydown", key);
});
}
function update(next) {
var prev = opts;
opts = Object.assign({ background: "#0c1e18", accent: "#e6ff55", sections: 4, duration: 900, parallax: 0.5, sensitivity: 40 }, opts, next || {});
opts.sections = Math.max(3, Math.min(6, Math.round(opts.sections)));
opts.duration = Math.max(0, opts.duration);
opts.parallax = Math.max(0, Math.min(1, opts.parallax));
opts.sensitivity = Math.max(1, opts.sensitivity);
wrap.style.background = opts.background;
wrap.style.setProperty("--ef-acc", opts.accent);
wrap.classList.toggle("is-rm", reduce);
if (prev.sections !== opts.sections || prev.accent !== opts.accent) build();
}
update(o);
return { update: update, destroy: function () { clear(); } };
}
// Usage
var fx = mount(document.querySelector("#my-root"), {"accent":"#e6ff55","sections":4,"duration":900,"parallax":0.5,"sensitivity":40});How it works
Every section is stacked in the same box and only the current one is visible. Input is gathered in one place, in the style of a GSAP Observer: wheel deltas are summed until they pass a threshold, a touch or pen swipe counts once it travels far enough, and the arrow, page, home and end keys jump directly. A busy flag and a short cool-down after each change ignore the rest of a trackpad flick, so one gesture moves one section. To change section, the incoming one is placed on top with a clip-path inset that hides it from the bottom edge or the top edge depending on direction, then transitioned to fully open. At the same time the content of the incoming section slides in from an offset while the outgoing content slides the other way. A fill bar, a counter and dot buttons follow the current index. The wheel listener sits on the element itself, so the page scrolls normally elsewhere, and at the first and last section the wheel is released so the page can scroll on.
Accessibility and performance
The element is a focusable group; the arrow, page, home and end keys change section and the dots are real buttons with labels. The counter is an aria-live region and inactive sections are hidden from assistive technology. With prefers-reduced-motion sections swap instantly with no wipe or parallax. On touch screens vertical swipes are used by the slider while the pointer is on it, so place it where that is acceptable and give users another route past it.
Only clip-path and transform are animated, on at most two sections at once, and the transition styles are removed when it ends. There is no scroll listener and no rAF loop; the wheel handler only does arithmetic. Avoid heavy filters on section backgrounds.
| Section | Full page |
|---|---|
| Techniques | Scroll-driven, Reveal and intro |
| Stack | Vanilla JS, CSS |
| Works with | React, Next.js, Vanilla |
| Dependencies | None |
| Status | tested |
| Inspired by | Animated Continuous Sections with GSAP Observer by GSAP 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.
More elements
FAQ
Can the wheel loop from the last section back to the first?
Not by default. At the ends the wheel is released so the page can scroll on. To loop, change the bounds in go() and in the wheel handler to wrap with a modulo.
Why does one trackpad flick only move one section?
A busy flag blocks input while the wipe runs and a short cool-down after it absorbs the inertia events that trackpads keep sending.
Does it need GSAP?
No. The input handling is a small hand-written version of the Observer idea and the animation is plain CSS transitions.