GSAP

Rasuke uses the GSAP npm package (3.15) for almost all motion. There is no AOS, Wow.js, or Swiper animation layer. Official docs: gsap.com

Registration

src/lib/gsap.ts is a client module that registers the plugins once and re-exports them. registerGsap() is idempotent and no-ops on the server.

'use client';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { ScrollSmoother } from 'gsap/ScrollSmoother';
import { ScrollToPlugin } from 'gsap/ScrollToPlugin';

let registered = false;
export function registerGsap() {
  if (registered || typeof window === 'undefined') return;
  gsap.registerPlugin(ScrollTrigger, ScrollSmoother, ScrollToPlugin);
  registered = true;
}

export { gsap, ScrollTrigger, ScrollSmoother, ScrollToPlugin };
Page-level ScrollSmoother

EffectsBootstrap creates one smoother per page on #smooth-wrapper / #smooth-content (both rendered by SiteShell):

ScrollSmoother.create({
  wrapper: "#smooth-wrapper",
  content: "#smooth-content",
  smooth: 1.35,          // one lag value for every input
  effects: true,         // enables data-speed / data-lag in markup
  smoothTouch: 1.35,
  ignoreMobileResize: true,
});
  • ScrollTrigger — pins, scrubs, and viewport reveals everywhere.
  • ScrollSmoother — inertia scrolling for the whole page. Skipped when prefers-reduced-motion: reduce is set or the host sets window.rasukeCore.smoothScroll = false / reducedMotion = true; the wrapper then falls back to native scroll without clipping.
  • ScrollToPlugin — back-to-top and anchor scrolling through the smoother.
  • SplitText — registered by the text modules (text-mark-top, text-mark-left, text-blur-stagger, text-flip, hero-2-title-rotate) for line / character splitting. SplitText is a GSAP Club plugin.
How effects attach

Each effect is a small ES module in src/effects, imported by EffectsBootstrap. Modules query a section class or a data-* hook and no-op when the markup is missing, so unused pages stay cheap. Entrance modules that must wait for the preloader (text-mark-*, projects-1, stagger-reveal, hero intros) are gated on the preloader's reveal / done promise; everything else boots immediately.

// src/components/effects/EffectsBootstrap.tsx (excerpt)
const preloader = initPreloader({ smoother });
initPageTransition({ smoother });
pushDestroy(initHero1Intro({ preloader }));
pushDestroy(initTextMarkTop());
pushDestroy(initGridDeform());
pushDestroy(initShowcase2());
// …every other initialiser; each returns a teardown function
  • preloader.js → preloader/Preloader.js — wordmark + counter + wipe reveal
  • transition.js — page collapse / grow transition between navigations
  • cursor.js — custom comet cursor with labelled discs
  • text-mark-top.js / text-mark-left.js / text-blur-stagger.js / text-flip.js — headline and link reveals
  • grid-wipe.js, stagger-reveal.js, anim-zoomin.js — generic reveal system
  • scroll-parallax.js, img-1-parallax.js, about-2-parallax.js, scroll-move-up.js — scrubbed parallax
  • marquee.js, digit-count.js — tickers and number rolls
  • showcase-1.js … showcase-6.js, projects-1.js, services-*.js, faqs-*.js — per-section interactions
App Router lifecycle

EffectsBootstrap runs inside useIsomorphicLayoutEffect keyed on usePathname(). On every route change it re-registers plugins, re-creates the smoother, re-initialises effects, resets scroll to the top (unless the URL has a hash), and refreshes ScrollTrigger. On unmount it runs every collected teardown, calls the module destroy* helpers (destroyAnimZoomIn, destroyDigitCount, destroyGridWipe, destroyStaggerReveal, destroyAwards1, destroyOffcanvasMenu), kills the smoother, and kills remaining ScrollTriggers.

Because React may re-mount effects (Strict Mode, fast refresh), destroy helpers restore the original DOM consumers (SplitText wrappers, digit ribbons, inline reveal styles) before React unmounts them.

Authoring rules
  • Keep animation in src/effects modules, not in components or inline scripts.
  • Pre-reveal states live in SCSS under the .js class (set by color-modes.js) so there is no flash of unstyled content; modules animate forward and hand elements back to CSS on complete.
  • Respect prefers-reduced-motion the same way existing modules do: write the final state immediately, or skip the effect.
  • Read durations and distances from CSS custom properties (motion and layout tokens) instead of hardcoding numbers in JS.
  • Return a cleanup function from new initialisers and push it into the bootstrap's cleanup list.
  • After structural DOM changes, call ScrollTrigger.refresh() if you add or remove triggers at runtime. The bootstrap already refreshes on mount, on load, and when webfonts are ready.