Skip to content
All docs
Building this site · 01

The motion system

How entrance motion works on supergeniuslabs.ai: one anime.js hook per surface, a bounded CSS pre-hide, IntersectionObserver reveals, and the GSAP line.

Updated

Two engines, one rule each. anime.js owns entrance motion on content pages. GSAP (with Lenis for smooth scroll) owns scroll-scrub, pinning, 3D, and custom eases. They never share a surface. This page is the contract; the code is components/motion/useAnimeMotion.ts.

One hook, one setup per surface

Every page with entrance motion mounts a small client component that calls useAnimeMotion(setup). The setup callback receives the anime API, the page root, and a push function for anything that must be reverted on navigation. The page stamps data-motion="name" on the elements that move; the setup queries them by name. Class names are styling, not a contract.

const settle: MotionSetup = (api, root, push) => {
  const title = target(root, 'title')
  if (title) title.style.opacity = '0'
  const tl = api.createTimeline({ defaults: { ease: 'outExpo' } })
  if (title) tl.add(title, { opacity: [0, 1], y: [16, 0], duration: 640 })
  push(tl)
  riseOnEnter(api, targets(root, 'block'), push)
}

The rules

RuleWhy
Import anime by subpath (animejs/animation, animejs/timeline, animejs/utils)The root entry is 25 KB gzipped; the three subpaths are 14 KB
Load after idle, dynamicallyanime is never in a route's First Load JS
IntersectionObserver, never anime's onScrollanime's scroll observer is dead under Lenis in production
Word splits are hand-rolledsplitText costs another 4.9 KB
CSS pre-hide keyed to data-motion-state="pending"Without it, targets paint at rest for a frame and then jump to 0
The pre-hide is boundedA CSS keyframe reveals pending targets after about 700 ms and flips visibility, so the LCP element never waits on the anime chunk. Measured on /pricing: mobile LCP 6.5 s to 2.5 s
Bail before import for reduced motion and E2EThose users never pay for the chunk; the attribute is stripped and the page is at rest
Push objects, never bare revert methodsanime's revert reads this; detached, it throws during client-side navigation
Anime-owned elements lose the generic .reveal classTwo engines would move one element twice
Count-ups restore the exact original textScreen readers and tests read final markup

One signature moment per page

The vocabulary is small on purpose: outExpo for settles, outQuart for scroll rises, everything under 700 ms per tween, transform and opacity only, nothing loops, paragraphs never animate. Each surface gets one moment someone might remember (the pricing count-up, the transcript cadence, the changelog ledger, the contact rail draw) and the rest stays quiet.

Where GSAP still lives

The home page's scroll narrative and anything that scrubs with scroll position. If a change needs ScrollTrigger, pinning, or WebGL, it is GSAP work and does not touch the anime hook.

Related

  • Design tokens: the durations and easings the timing vocabulary is built from.