Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Create cinematic scroll-driven landing pages with Lenis smooth scrolling, GSAP ScrollTrigger, scroll-linked progression, staggered text reveals, sticky card stacks, parallax backgrounds, scroll-scrubbed transitions, footer reveals, and immersive preloaders. Use when analyzing or building premium editorial scroll experiences, sticky project stacks, kinetic typography, or section-by-section storytelling.
.claude/skills/mengto-cinematic-scroll-storytelling/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | 116% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 138% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 119% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 80% | 0% |
| case-21 | ✗→✓ | ▲ Improved | 112% | 0% |
scrub.scrub: true or scrub: 1.Avoid:
bashnpm i gsap lenis
jsimport Lenis from "lenis"; import "lenis/dist/lenis.css"; import { gsap } from "gsap"; import { ScrollTrigger } from "gsap/ScrollTrigger"; gsap.registerPlugin(ScrollTrigger); const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; if (!reduceMotion) { const lenis = new Lenis({ lerp: 0.08, smoothWheel: true, wheelMultiplier: 0.9, }); lenis.on("scroll", ScrollTrigger.update); gsap.ticker.add((time) => { lenis.raf(time * 1000); }); gsap.ticker.lagSmoothing(0); } window.addEventListener("load", () => ScrollTrigger.refresh());
power3.out or power4.out.ease: "none" with scrub: 0.8 to 1.4.0.8s to 1.1s.0.9s to 1.2s.0.035s to 0.07s.0.08s to 0.14s.0.06s to 0.1s.y: 24 to 48.4px to 10px, then 0px.1 down to 0.92.html<div class="preloader" data-preloader> <div class="preloader__bar" data-preloader-bar></div> </div> <main> <section class="hero" data-parallax-section> <img data-parallax-layer data-speed="-0.18" src="/hero.jpg" alt=""> <h1 data-split-reveal>Design that unfolds with cinematic restraint.</h1> </section> <section data-story-section> <p data-split-reveal="words">Every block arrives with quiet intent.</p> </section> <section class="project-stack" data-sticky-stack> <article data-stack-card>Project One</article> <article data-stack-card>Project Two</article> <article data-stack-card>Project Three</article> </section> <footer data-footer-parallax>...</footer> </main>
Use a preloader to set the cinematic tone, then hand off into the hero reveal.
jsfunction initPreloader() { const loader = document.querySelector("[data-preloader]"); const bar = document.querySelector("[data-preloader-bar]"); if (!loader) return Promise.resolve(); if (reduceMotion) { loader.remove(); return Promise.resolve(); } return new Promise((resolve) => { const tl = gsap.timeline({ defaults: { ease: "power3.out" }, onComplete: () => { loader.remove(); resolve(); }, }); tl.fromTo(bar, { scaleX: 0, transformOrigin: "left" }, { scaleX: 1, duration: 1.1 }) .to(loader, { yPercent: -100, duration: 0.9, ease: "power4.inOut" }, "+=0.15"); }); }
Use masked overflow containers. Avoid splitting text that contains links or meaningful inline markup.
jsfunction splitWords(element) { if (element.dataset.splitReady === "true") return; const text = element.textContent || ""; const parts = text.split(/(\s+)/); element.textContent = ""; element.setAttribute("aria-label", text.trim()); parts.forEach((part) => { if (!part.trim()) { element.appendChild(document.createTextNode(part)); return; } const mask = document.createElement("span"); const word = document.createElement("span"); mask.className = "split-word-mask"; word.className = "split-word"; word.textContent = part; mask.setAttribute("aria-hidden", "true"); mask.appendChild(word); element.appendChild(mask); }); element.dataset.splitReady = "true"; } function initSplitReveals() { if (reduceMotion) { gsap.set("[data-split-reveal]", { autoAlpha: 1 }); return; } gsap.utils.toArray("[data-split-reveal]").forEach((element) => { splitWords(element); const words = element.querySelectorAll(".split-word"); gsap.fromTo( words, { yPercent: 110, autoAlpha: 0, filter: "blur(8px)" }, { yPercent: 0, autoAlpha: 1, filter: "blur(0px)", duration: 0.95, ease: "power4.out", stagger: 0.05, scrollTrigger: { trigger: element, start: "top 82%", once: true, }, } ); }); }
css.split-word-mask { display: inline-block; overflow: hidden; vertical-align: top; } .split-word { display: inline-block; will-change: transform, opacity, filter; }
Use these for normal sections. They should play once and feel composed, not twitchy.
jsfunction initSectionReveals() { if (reduceMotion) { gsap.set("[data-story-section], [data-reveal-item]", { autoAlpha: 1, clearProps: "all" }); return; } gsap.utils.toArray("[data-story-section]").forEach((section) => { const items = section.querySelectorAll("[data-reveal-item]"); const targets = items.length ? items : section.children; gsap.fromTo( targets, { y: 36, autoAlpha: 0, filter: "blur(8px)" }, { y: 0, autoAlpha: 1, filter: "blur(0px)", duration: 1, ease: "power4.out", stagger: 0.08, scrollTrigger: { trigger: section, start: "top 82%", once: true, }, } ); }); }
Use scrubbed timelines for cinematic progression. Keep scrubbed animation linear and let the scroll position do the timing.
jsfunction initProgressionScenes() { if (reduceMotion) return; gsap.utils.toArray("[data-progress-scene]").forEach((scene) => { const media = scene.querySelector("[data-progress-media]"); const copy = scene.querySelectorAll("[data-progress-copy]"); gsap.timeline({ scrollTrigger: { trigger: scene, start: "top top", end: "+=140%", scrub: 1.1, pin: true, anticipatePin: 1, }, }) .fromTo(media, { scale: 1.08 }, { scale: 1, ease: "none" }) .fromTo(copy, { autoAlpha: 0, y: 40 }, { autoAlpha: 1, y: 0, stagger: 0.15, ease: "none" }, 0.15); }); }
Use position: sticky for layout, and ScrollTrigger for layered scale/depth. Earlier cards should recede as later cards arrive.
css[data-sticky-stack] { position: relative; } [data-stack-card] { position: sticky; top: 12vh; transform-origin: center top; will-change: transform, opacity; }
jsfunction initStickyCardStack() { if (reduceMotion) return; gsap.utils.toArray("[data-sticky-stack]").forEach((stack) => { const cards = gsap.utils.toArray(stack.querySelectorAll("[data-stack-card]")); cards.forEach((card, index) => { const nextCard = cards[index + 1]; if (!nextCard) return; gsap.to(card, { scale: 0.92 + index * 0.015, autoAlpha: 0.72, y: -24, ease: "none", scrollTrigger: { trigger: nextCard, start: "top 78%", end: "top 24%", scrub: true, invalidateOnRefresh: true, }, }); }); }); }
Use parallax for hero images, background layers, and footer reveals. Keep distance small.
jsfunction initParallax() { if (reduceMotion) return; gsap.utils.toArray("[data-parallax-layer]").forEach((layer) => { const speed = Number(layer.dataset.speed || -0.16); const section = layer.closest("[data-parallax-section]") || layer; gsap.to(layer, { y: () => window.innerHeight * speed, ease: "none", scrollTrigger: { trigger: section, start: "top bottom", end: "bottom top", scrub: 1, invalidateOnRefresh: true, }, }); }); }
Footer parallax reveal:
jsfunction initFooterReveal() { if (reduceMotion) return; const footer = document.querySelector("[data-footer-parallax]"); if (!footer) return; gsap.fromTo( footer, { yPercent: -12, autoAlpha: 0.85 }, { yPercent: 0, autoAlpha: 1, ease: "none", scrollTrigger: { trigger: footer, start: "top bottom", end: "top 45%", scrub: 1, }, } ); }
txtCreate a cinematic scroll-driven landing page with smooth Lenis scrolling, GSAP ScrollTrigger animations, staggered text reveals, sticky card stack progression, parallax backgrounds, scroll-scrubbed transitions, section-by-section storytelling, and an immersive preloader animation. Use layered depth, scaling transitions, progressive opacity changes, and smooth viewport-triggered motion for a premium editorial experience.
scrub intentionally.ScrollTrigger.refresh() runs after images/fonts/layout shifts.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 25,798 | 23,801 | -8% | 1 | 1 | 0% | 6,226 | 9,632 | +55% | 0 | 0 | — |
case-02 | fail→fail | 24,882 | 24,718 | -1% | 1 | 1 | 0% | 5,865 | 9,612 | +64% | 0 | 0 | — |
case-03 | fail→pass | 12,414 | 10,793 | -13% | 1 | 1 | 0% | 2,634 | 5,690 | +116% | 0 | 0 | — |
case-04 | pass→pass | 13,569 | 12,740 | -6% | 1 | 1 | 0% | 3,073 | 6,296 | +105% | 0 | 0 | — |
case-05 | pass→pass | 11,238 | 11,139 | -1% | 1 | 1 | 0% | 2,558 | 6,073 | +137% | 0 | 0 | — |
case-06 | pass→pass | 16,727 | 13,734 | -18% | 1 | 1 | 0% | 3,731 | 6,537 | +75% | 0 | 0 | — |
case-07 | pass→pass | 11,356 | 8,007 | -29% | 1 | 1 | 0% | 2,240 | 5,027 | +124% | 0 | 0 | — |
case-08 | fail→fail | 9,304 | 5,984 | -36% | 1 | 1 | 0% | 1,975 | 4,676 | +137% | 0 | 0 | — |
case-09 | pass→pass | 13,734 | 5,750 | -58% | 1 | 1 | 0% | 2,549 | 4,529 | +78% | 0 | 0 | — |
case-10 | fail→fail | 12,119 | 10,691 | -12% | 1 | 1 | 0% | 2,025 | 5,282 | +161% | 0 | 0 | — |
case-11 | pass→pass | 11,735 | 4,058 | -65% | 1 | 1 | 0% | 2,113 | 4,095 | +94% | 0 | 0 | — |
case-12 | fail→fail | 11,429 | 11,061 | -3% | 1 | 1 | 0% | 2,167 | 5,419 | +150% | 0 | 0 | — |
case-13 | fail→fail | 17,178 | 10,639 | -38% | 1 | 1 | 0% | 3,478 | 5,602 | +61% | 0 | 0 | — |
case-14 | fail→pass | 13,737 | 14,641 | +7% | 1 | 1 | 0% | 2,719 | 6,484 | +138% | 0 | 0 | — |
case-15 | fail→pass | 11,234 | 6,479 | -42% | 1 | 1 | 0% | 2,180 | 4,773 | +119% | 0 | 0 | — |
case-16 | pass→pass | 12,957 | 4,463 | -66% | 1 | 1 | 0% | 2,374 | 4,273 | +80% | 0 | 0 | — |
case-17 | pass→pass | 6,806 | 5,235 | -23% | 1 | 1 | 0% | 1,237 | 4,365 | +253% | 0 | 0 | — |
case-18 | fail→pass | 15,901 | 10,600 | -33% | 1 | 1 | 0% | 2,989 | 5,372 | +80% | 0 | 0 | — |
case-19 | pass→pass | 13,456 | 9,313 | -31% | 1 | 1 | 0% | 2,431 | 5,119 | +111% | 0 | 0 | — |
case-20 | pass→pass | 13,399 | 11,260 | -16% | 1 | 1 | 0% | 2,591 | 5,579 | +115% | 0 | 0 | — |
case-21 | fail→pass | 12,268 | 6,487 | -47% | 1 | 1 | 0% | 2,182 | 4,615 | +112% | 0 | 0 | — |
case-22 | pass→pass | 16,199 | 14,742 | -9% | 1 | 1 | 0% | 2,571 | 5,844 | +127% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +23 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.