Home
cd ../playbooks
Developer ToolsAdvanced

Cinematic GSAP + Lenis Motion System

Build a connected, premium web motion language with GSAP, ScrollTrigger, and Lenis smooth scroll — a full token system for eases/durations/staggers, Lenis-driven-by-the-GSAP-ticker setup, a reusable data-attribute markup API for text reveals and parallax, and reduced-motion handling built in from the start, not bolted on after.

15 minutes
By MengTo (Skills)Source
#gsap#scrolltrigger#lenis#web-animation#smooth-scroll#frontend

Awwwards-style sites don't feel premium because of one flashy effect — they feel premium because every scroll reveal, hover, and cursor movement shares the same restrained eases and timings, which is exactly what falls apart when different animations get added ad hoc by different people over time.

Who it's for: frontend developers building luxury editorial, creative studio, or portfolio websites, teams wanting Awwwards-caliber scroll and hover motion without an inconsistent grab-bag of animation libraries, engineers setting up Lenis smooth scroll alongside GSAP ScrollTrigger for the first time, anyone who needs a reusable data-attribute API so designers can add scroll reveals without touching JavaScript

Example

"Build a cinematic scroll experience for this studio portfolio site" → Lenis initialized and synced through the GSAP ticker so smooth scroll and ScrollTrigger never drift apart, a masked staggered-word text reveal on the hero with correct aria-label handling for screen readers, image parallax and magnetic hover wired through simple data-reveal/data-magnetic attributes, and every animation gated behind prefers-reduced-motion from the first line of setup

CLAUDE.md Template

New here? 3-minute setup guide → | Already set up? Copy the template below.

# Cinematic GSAP + Lenis Motion System

Build a premium, cinematic web motion system with GSAP, ScrollTrigger, and Lenis smooth scroll. Use for luxury editorial websites, creative studio portfolios, Awwwards-style interactions, smooth scroll reveals, staggered text, parallax, pinned sections, magnetic hover states, and custom cursors.

## Use When

- The site needs a full premium motion language, not one isolated animation.
- Smooth scrolling, scroll reveals, pinned scenes, parallax, hover motion, and cursor behavior should all feel connected.
- The target feel is luxury editorial, Apple-level polish, a creative studio portfolio, or immersive cinematic storytelling.

## Motion Taste

Smooth, elegant, slightly delayed, and intentional. Staggered motion should guide reading order. Layered movement should create depth without making the interface feel busy. `ScrollTrigger` should start scenes when they enter the viewport, not react to every tiny scroll. Prefer subtlety over intensity.

Avoid: bounce, elastic, springy, or playful motion; fast abrupt transitions; large scale jumps; over-animated UI; flashy gaming-style effects.

## Base Tokens

- Eases: `power3.out`, `power4.out`, `expo.out`.
- Scroll scrub: `scrub: 0.8` to `1.4` for cinematic delay.
- Reveals: `0.75s` to `1.1s`.
- Hover: `0.35s` to `0.6s`.
- Cursor lag: `0.25s` to `0.45s`.
- Text stagger: words `0.035s` to `0.07s`, lines `0.08s` to `0.14s`.
- Card stagger: `0.06s` to `0.1s`.
- Reveal trigger: `start: "top 82%"`.
- Pin handoff: `anticipatePin: 1`.

## Setup

```bash
npm i gsap lenis
```

Initialize once, after the DOM exists. Lenis drives its RAF through the GSAP ticker so `ScrollTrigger` and smooth scroll stay synced.

```js
import Lenis from "lenis";
import "lenis/dist/lenis.css";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";

gsap.registerPlugin(ScrollTrigger);
gsap.defaults({ ease: "power3.out", duration: 0.85 });

const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

let lenis;

if (!reduceMotion) {
  lenis = new Lenis({
    lerp: 0.08,
    smoothWheel: true,
    wheelMultiplier: 0.9,
    anchors: true,
  });

  lenis.on("scroll", ScrollTrigger.update);

  gsap.ticker.add((time) => {
    lenis.raf(time * 1000);
  });

  gsap.ticker.lagSmoothing(0);
}

window.addEventListener("load", () => {
  ScrollTrigger.refresh();
});
```

## Markup API

Use small data attributes so the motion system can be reused across pages.

```html
<h1 data-motion-text="lines">Digital products with cinematic restraint.</h1>
<p data-motion-text="words">Every interaction should feel deliberate.</p>

<section data-reveal-group>
  <article data-reveal="fade-up" data-reveal-item>...</article>
  <article data-reveal="fade-up" data-reveal-item>...</article>
</section>

<figure data-image-reveal data-parallax-section>
  <img data-parallax-image src="/studio.jpg" alt="">
</figure>

<a data-magnetic data-cursor-label="Explore" href="/work">Explore</a>
<div data-cursor><span data-cursor-label></span></div>
```

## CSS Foundation

```css
html.has-motion [data-motion-text],
html.has-motion [data-reveal],
html.has-motion [data-reveal-item],
html.has-motion [data-image-reveal] {
  visibility: hidden;
}

.motion-line-mask,
.motion-word-mask {
  display: inline-block;
  overflow: hidden;
  vertical-align: top;
}

.motion-line,
.motion-word {
  display: inline-block;
  will-change: transform, opacity, filter;
}

[data-image-reveal] { overflow: hidden; }

[data-parallax-image] {
  display: block;
  width: 100%;
  height: 115%;
  object-fit: cover;
  will-change: transform;
}

[data-cursor] {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 9999;
  pointer-events: none;
  mix-blend-mode: difference;
  transform: translate3d(-50%, -50%, 0);
  will-change: transform;
}

@media (prefers-reduced-motion: reduce), (pointer: coarse) {
  [data-cursor] { display: none; }
}
```

## Staggered Text Reveals

Use masked containers for premium text. Prefer manual line wrappers when exact line breaks matter; use word splitting for flexible responsive text.

```js
document.documentElement.classList.add("has-motion");

function splitWords(element) {
  if (element.dataset.motionSplit === "true") return;
  const text = element.textContent || "";
  const parts = text.split(/(\s+)/);
  element.textContent = "";
  element.setAttribute("aria-label", text.trim());
  let index = 0;
  parts.forEach((part) => {
    if (!part.trim()) {
      element.appendChild(document.createTextNode(part));
      return;
    }
    const mask = document.createElement("span");
    const word = document.createElement("span");
    mask.className = "motion-word-mask";
    mask.setAttribute("aria-hidden", "true");
    word.className = "motion-word";
    word.textContent = part;
    word.style.setProperty("--word-index", index);
    mask.appendChild(word);
    element.appendChild(mask);
    index += 1;
  });
  element.dataset.motionSplit = "true";
}

function initTextReveals() {
  if (reduceMotion) {
    gsap.set("[data-motion-text]", { autoAlpha: 1, clearProps: "all" });
    return;
  }

  gsap.utils.toArray("[data-motion-text='words']").forEach((element) => {
    splitWords(element);
    const words = element.querySelectorAll(".motion-word");
    gsap.set(element, { autoAlpha: 1 });
    gsap.fromTo(
      words,
      { yPercent: 110, autoAlpha: 0, filter: "blur(8px)" },
      {
        yPercent: 0,
        autoAlpha: 1,
        filter: "blur(0px)",
        duration: 0.9,
        ease: "power4.out",
        stagger: 0.055,
        scrollTrigger: { trigger: element, start: "top 82%", once: true },
      }
    );
  });
}
```

`aria-label` is set to the original text before splitting, and `aria-hidden="true"` is applied to the visual mask spans, so screen readers announce the real sentence instead of individually split words.

## Parallax and Magnetic Hover

For image parallax, scale the image slightly oversized (`height: 115%`) and translate it on scroll via `ScrollTrigger` with a `scrub` value from the token list above — never animate `top`/`margin` for this, only `transform`.

For magnetic hover on `[data-magnetic]` elements, track pointer position relative to the element's bounding box and translate the element toward the pointer within a clamped range (a few pixels to ~15% of the element's size), easing back to center with the hover-duration token on `mouseleave`. Pair it with the custom cursor: update `[data-cursor]` position on `mousemove` with the cursor-lag token, and swap its label text via `data-cursor-label` on hover targets.

## Respect Motion Preferences

Every effect above checks `reduceMotion` before initializing. Under `prefers-reduced-motion: reduce`, skip Lenis entirely (native scroll), set all `[data-motion-text]`/`[data-reveal]` elements straight to their final visible state instead of animating in, and hide the custom cursor. This is not optional — the CSS foundation hides motion-driven elements by default specifically so a reduced-motion visitor never sees a flash of hidden content if JavaScript fails to run.

Get new playbooks like this one

One email a week with new Claude Code workflows. Free, like everything here.

No spam. Unsubscribe anytime.

README.md

What This Does

A complete, token-driven motion system for building the kind of restrained, cinematic scroll experience common to award-winning creative and editorial websites — GSAP for the animation engine, ScrollTrigger for scroll-driven timing, and Lenis for the smooth-scroll feel, wired together correctly so Lenis drives its render loop through the GSAP ticker instead of running its own competing requestAnimationFrame loop. A full set of design tokens (eases like power3.out/power4.out, scrub ranges, reveal/hover/cursor-lag durations, word and line stagger timings) keeps every effect on the site feeling like one motion language instead of a grab-bag of unrelated animations — with an explicit list of what to avoid entirely: bounce, elastic springiness, abrupt transitions, and anything reading as "flashy" rather than deliberate.

The reusable part is a small, designer-friendly markup API — data-motion-text="words", data-reveal="fade-up", data-parallax-image, data-magnetic — so scroll reveals, staggered text, image parallax, and magnetic hover states can be added to new markup without writing new JavaScript each time, backed by working implementation code for the word-splitting text-reveal function (including the aria-label handling that keeps split-apart text announced correctly to screen readers). Accessibility is structural, not an afterthought: the CSS foundation hides motion-driven elements by default specifically so a reduced-motion visitor never sees a flash of unstyled content, and every effect checks prefers-reduced-motion before initializing at all.


Quick Start

Step 1: Create a Project Folder

mkdir cinematic-motion && cd cinematic-motion

Step 2: Download the Template

Click Download above, then:

mv ~/Downloads/CLAUDE.md ./

Step 3: Build the Motion System

claude

Ask Claude to set up cinematic scroll motion for your site, add a staggered text reveal, or wire up parallax and magnetic hover. It will install GSAP and Lenis, initialize them correctly synced, and use the design-token values and data-attribute API so every effect stays consistent with the rest of the site.


Tips & Best Practices

  • Never initialize Lenis with its own separate RAF loop — driving it through gsap.ticker.add() as shown is what keeps ScrollTrigger and the smooth-scroll position from drifting apart on longer pages.
  • Stick to the token ranges rather than picking arbitrary duration values per effect; the entire point of the system is that a hover, a reveal, and a parallax scrub all feel like they belong to the same site.
  • Test with prefers-reduced-motion: reduce turned on before calling any animation done — the CSS foundation's default-hidden state means a broken reduced-motion path can leave content invisible, not just un-animated.

Limitations

  • A GSAP + Lenis-specific system — the token values and setup code don't transfer directly to a different animation stack (Framer Motion, native CSS-only approaches), though the underlying motion-taste principles (restraint, paired timing, avoid bounce) generalize.
  • Best suited to editorial, portfolio, and brand-forward sites where an elaborate motion language is the point; a dense data/utility product UI should reach for the simpler, faster timings in a general web-animation-design guide instead.
  • The magnetic-hover and custom-cursor patterns assume a pointer-driven desktop experience — both are explicitly disabled on coarse (touch) pointers and under reduced motion, so touch users get a plainer but fully functional experience by design.

$Related Playbooks

Developer Tools

Contribution PR Review

Review an external contributor's PR the way an open-source maintainer should — check automated bot security findings first (across every channel they might post to), watch specifically for suspicious AGENTS.md or workflow-permission changes, scale size and test-coverage expectations by contributor experience, and separate intent/scope alignment from code quality that bots already covered.

10 minutes
Intermediate
Developer Tools

Datadog Error Triage to PR

Take Datadog Error Tracking issues from raw occurrence counts to a reviewable, tested PR — a five-class bug taxonomy that filters out infra noise and deploy-skew before ranking, a recency premium that catches fresh regressions early, a mandatory approval gate before any fix is written, and a test-first fix workflow that generalizes past the one reported occurrence.

10 minutes
Advanced
Developer Tools

Documentation Review System

Mode-routed documentation review that diagnoses a page against its doc type and seven quality dimensions before touching a sentence, then picks the intervention level — maintenance, improve, rewrite, author, or strategy — that actually matches what's wrong.

10 minutes
Intermediate
Developer Tools

Designer's Figma-to-Production Workflow

A structured discuss-plan-execute-verify loop that takes a designer from a Figma file to a deployed, pixel-perfect production site — using Claude Code plus the GSD meta-prompting system, no traditional coding required.

20 minutes
Intermediate
Developer Tools

Distinctive Frontend Design

Design lead guidance for building frontends that don't read as AI-generated — deliberate palette, typography, and layout choices grounded in the actual subject, with a built-in self-critique pass before you write code.

5 minutes
Intermediate
Developer Tools

Continue Claude Work

Recover actionable context from local .claude session artifacts and continue interrupted work — without running claude --resume — by inspecting history first.

10 minutes
Intermediate
Developer Tools

Network Issue Debugging

Apply falsification-first, layered isolation to pin down the responsible network layer for connection resets, SSE stalls, and fixed-time drops — instead of stacking assumptions.

20 minutes
Advanced
Developer Tools

Docs Cleaner

Consolidate redundant documentation while preserving all valuable content — merge overlapping files, reduce sprawl, and cut bloat without losing anything important.

10 minutes
Beginner
Developer Tools

Database Sync Manager

Automate database synchronization, replication, migration, and cross-platform data integration

10 minutes
Advanced
Developer Tools

Developer Presentation Builder

Create developer-focused presentations with live code demos and diagrams using Slidev.

10 minutes
Advanced
Developer Tools

DevOps Automation Assistant

DevOps and IT Ops automation - CI/CD, monitoring, incident management, and infrastructure workflows

10 minutes
Advanced
Developer Tools

Discord Bot Builder

Discord bot development - community management, moderation, notifications, and AI integration

10 minutes
Advanced

Browse all Developer Tools playbooks →