npx skills add ...
npx skills add mengto/skills --skill cinematic-scroll-storytelling
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.
npx skills add mengto/skills --skill cinematic-scroll-storytelling
scrub.scrub: true or scrub: 1.Avoid:
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.Use a preloader to set the cinematic tone, then hand off into the hero reveal.
Use masked overflow containers. Avoid splitting text that contains links or meaningful inline markup.
Use these for normal sections. They should play once and feel composed, not twitchy.
Use scrubbed timelines for cinematic progression. Keep scrubbed animation linear and let the scroll position do the timing.
Use position: sticky for layout, and ScrollTrigger for layered scale/depth. Earlier cards should recede as later cards arrive.
Use parallax for hero images, background layers, and footer reveals. Keep distance small.
Footer parallax reveal:
scrub intentionally.ScrollTrigger.refresh() runs after images/fonts/layout shifts.<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>function 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");
});
}function 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,
},
}
);
});
}.split-word-mask {
display: inline-block;
overflow: hidden;
vertical-align: top;
}
.split-word {
display: inline-block;
will-change: transform, opacity, filter;
}function 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,
},
}
);
});
}function 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);
});
}[data-sticky-stack] {
position: relative;
}
[data-stack-card] {
position: sticky;
top: 12vh;
transform-origin: center top;
will-change: transform, opacity;
}function 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,
},
});
});
});
}function 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,
},
});
});
}function 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,
},
}
);
}Create 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.