Skip to Content
StatixFlow Next Docs
Animation System

Animation System

Scroll and intro animations are driven by declarative data-* attributes rather than bespoke GSAP code written per section. To add a scroll effect to a section, add one of the attributes below to a JSX element — don’t write a new useGSAP call for it.

Architecture

  • lib/gsap.ts — registers GSAP plugins once (useGSAP, ScrollTrigger, ScrollSmoother, SplitText) and re-exports them. Import GSAP from here, not directly from gsap.
  • SiteMotionProvider (components/animations/SiteMotionProvider.tsx) — a React context (useSiteMotion) holding shared state: headerRef (the header DOM node), smootherRef (the live ScrollSmoother instance), and a scrollTo(target, duration?) helper that smooth-scrolls to an in-page anchor, briefly disabling the header’s scroll-driven animation while it does.
  • SiteAnimations (components/animations/SiteAnimations.tsx) — mounted once by SiteShell. It owns the ScrollSmoother wrapper/content DOM structure (#smooth-wrapper / #smooth-content) and contains the single useGSAP block that scans its scope for every data-* attribute below and wires up the matching effect. This runs once fonts are ready (gated on document.fonts.ready), and on cleanup it reverts all SplitText instances, kills all ScrollTriggers, and removes the intro-animation-blocking gsap-intro-pending class.

data-* attribute reference

AttributeWhere it goesWhat it does
data-page-intro-animationWraps a section that plays an entrance animation on loadRoot marker; scoped children below animate once fonts are ready
data-page-intro-animation-unmask-textOn a text element inside a data-page-intro-animation rootSplits the text into lines and reveals them with a masked upward reveal (SplitText, yPercent from -unmask-y-percent, default 120)
data-page-intro-animation-move-upOn any element inside a data-page-intro-animation rootFades/slides the element up on load (staggered)
data-highlight-text-on-scrollOn a text blockSplits into words and fades each word in on scroll (scrubbed) as the block enters the viewport
data-unmask-text-on-scrollOn a text blockSame masked-line reveal as the intro variant, but scroll-scrubbed instead of load-triggered
data-move-up-on-scrollOn any elementFades/slides the element up as it scrolls into view (scrubbed)
data-parallax-image-on-scrollWraps an <img>Parallax-shifts the image vertically as its container scrolls
data-highlight-on-scrollWraps a set of a[href^="#"] links + matching headingsMarks the link matching the currently-in-view heading with data-highlight-on-scroll-active-link (for TOC-style active-state highlighting)
data-pinned-sideWraps content that should pin while a sibling column scrollsPins the element under the header while its data-pinned-side-container ancestor scrolls past; supports data-pinned-side-screen (sm/md/lg/xl/2xl) to only pin above a breakpoint
data-pinned-side-containerThe scroll-length ancestor for a data-pinned-side elementDefines how long the pin lasts

Most attributes accept -start / -end scrub-position overrides (e.g. data-move-up-on-scroll-start, data-move-up-on-scroll-end) and numeric tuning attributes (e.g. -y-percent, -stagger, -duration) — read the relevant block in SiteAnimations.tsx for the exact defaults before overriding one.

Example

<div data-move-up-on-scroll data-move-up-on-scroll-start="top 90%"> <h2 data-highlight-text-on-scroll>This fades in word by word on scroll.</h2> </div>

Adding a new scroll effect

Prefer adding a new data-* attribute consumed by the existing useGSAP block in SiteAnimations.tsx over writing a bespoke effect elsewhere. This keeps every scroll animation discoverable in one file and means new sections opt into effects by attribute, not by importing animation code.

The intro-pending class

app/layout.tsx puts a gsap-intro-pending class on <html> from the very first paint:

app/layout.tsx
<html lang="en" className={`${instrumentSans.variable} gsap-intro-pending`}>

While present, this CSS rule in app/globals.css keeps intro-animation targets invisible so they don’t flash in unanimated before GSAP is ready:

app/globals.css
html.gsap-intro-pending [data-page-intro-animation-unmask-text], html.gsap-intro-pending [data-page-intro-animation-move-up] { opacity: 0; }

SiteAnimations removes the class itself once fonts are ready and the intro useGSAP block runs (document.documentElement.classList.remove("gsap-intro-pending")). Don’t remove this class from layout.tsx without also updating this CSS rule — doing so would make intro-animated content flash visible before its animation plays.

Smooth scrolling and in-page anchors

SiteAnimations wires up ScrollSmoother and intercepts clicks on same-page anchor links (a[href^="#"]) inside its scope, routing them through scrollTo instead of the browser’s native jump. If you add a custom anchor link outside this flow, use the useSmoothAnchorClick() hook (exported from SiteAnimations.tsx) as its onClick handler to keep it consistent with the rest of the site.

Last updated on