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: reduceis set or the host setswindow.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 revealtransition.js— page collapse / grow transition between navigationscursor.js— custom comet cursor with labelled discstext-mark-top.js/text-mark-left.js/text-blur-stagger.js/text-flip.js— headline and link revealsgrid-wipe.js,stagger-reveal.js,anim-zoomin.js— generic reveal systemscroll-parallax.js,img-1-parallax.js,about-2-parallax.js,scroll-move-up.js— scrubbed parallaxmarquee.js,digit-count.js— tickers and number rollsshowcase-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/effectsmodules, not in components or inline scripts. - Pre-reveal states live in SCSS under the
.jsclass (set bycolor-modes.js) so there is no flash of unstyled content; modules animate forward and hand elements back to CSS on complete. - Respect
prefers-reduced-motionthe 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, onload, and when webfonts are ready.