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
| Rule | Why |
|---|---|
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, dynamically | anime is never in a route's First Load JS |
IntersectionObserver, never anime's onScroll | anime's scroll observer is dead under Lenis in production |
| Word splits are hand-rolled | splitText 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 bounded | A 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 E2E | Those users never pay for the chunk; the attribute is stripped and the page is at rest |
Push objects, never bare revert methods | anime's revert reads this; detached, it throws during client-side navigation |
Anime-owned elements lose the generic .reveal class | Two engines would move one element twice |
| Count-ups restore the exact original text | Screen 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.
