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

Claude Code Project Architecture

A setup guide for structuring a Claude Code project's full toolkit — layered CLAUDE.md context, slash commands, sub-agents, skills, hooks, and plugins — each used for what it's actually good at instead of one mechanism for everything.

20 minutes
Intermediate
Developer Tools

Claude Model Migration Assistant

Migrate code and prompts between Claude models — update model strings, remove incompatible beta headers, and adjust prompts only for reported behavioral differences.

5 minutes
Beginner
Developer Tools

Claude Code Plugin Builder

End-to-end guided workflow for building Claude Code plugins — structure, commands, agents, skills, hooks, and MCP integration — with a validation checklist before you distribute.

15 minutes
Advanced
Developer Tools

CLAUDE.md Progressive Disclosurer

Slim down and restructure a bloated CLAUDE.md (or AGENTS.md) using progressive disclosure — move low-frequency detail to Level 2 references while keeping Level 1 lean, with zero information loss.

15 minutes
Intermediate
Developer Tools

Claude Usage Analyst

Analyze Claude Code token usage, cost, quota burn, model mix, and cache read/write using ccusage evidence — and explain in human terms why your quota got exhausted.

10 minutes
Beginner
Developer Tools

CLI Demo Generator

Generate professional animated CLI demos as GIFs using VHS terminal recordings — with self-bootstrapping setup, output filtering, and frame-level verification.

15 minutes
Intermediate
Developer Tools

Cloudflare Troubleshooting

Investigate and resolve Cloudflare issues with API-driven evidence — ERR_TOO_MANY_REDIRECTS, SSL mode mismatches, DNS, and proxy problems — instead of guessing.

15 minutes
Intermediate
Developer Tools

Changelog Generator

Generate release notes from git commits, updates, or feature lists

10 minutes
Advanced
Developer Tools

CI/CD Pipeline Generator

Create production-ready CI/CD pipeline configurations for GitHub Actions, GitLab CI, CircleCI, and Jenkins with deployment to Vercel, Netlify, or AWS.

15 minutes
Intermediate
Developer Tools

Codebase Documenter

Create comprehensive, beginner-friendly documentation for codebases including READMEs, architecture guides, API docs, and code comments.

10 minutes
Beginner
Developer Tools

Composio SDK with Claude Code: Connect 250+ APIs in Minutes

Connect Claude Code to 250+ APIs using Composio SDK. Build AI agents that send emails, create GitHub issues, and post to Slack — not just generate text.

15 minutes
Advanced
Developer Tools

Error Message Explainer — Decode Any Error in Plain English

Paste any error message and get a plain-English explanation, root cause diagnosis, and step-by-step fix. Works with Python, JavaScript, Docker, Git, and any CLI error.

5 minutes
Beginner

Browse all Developer Tools playbooks →