Writing

Motion with intent

Jun 12, 20262 min readReact NativeMotionPerformance

Animation on the web has a bad reputation for the same reason it has a bad reputation in mobile apps: most of it is decoration that costs frames. The rule I build by — on this site, and in every app I ship — is that motion must carry information. Things enter the way your eye expects them to, and everything else stays still.

The budget comes first

Every effect on this site had to pass three checks before it shipped:

  • It renders at 60fps on a mid-range phone, not just a MacBook.
  • It disables itself completely under prefers-reduced-motion.
  • It never blocks the initial paint — the three.js scenes are loaded with next/dynamic and ssr: false, so the hero text is readable before a single byte of WebGL arrives.

The render loops are just as defensive. Each canvas pauses itself when it scrolls offscreen or the tab is hidden:

const io = new IntersectionObserver(([entry]) => {
  entry.isIntersecting ? play() : pause();
});
io.observe(canvas);

document.addEventListener("visibilitychange", () =>
  document.hidden ? pause() : play(),
);

An ambient effect that drains a battery while nobody is looking at it is not ambience — it's a leak.

Respecting reduced motion is not optional

Accessibility settings are requirements, not suggestions. A user who asked for less motion should get a site that looks designed, not a site with holes in it.

Every animated component here has a static counterpart: the orbiting atom falls back to its still glyph, the particle field simply doesn't mount, and scroll reveals collapse to plain visibility. The fallback is designed first; the animation is layered on top.

The same craft, two platforms

This is the same discipline React Native demands. The JS thread budget on a low-end Android phone is unforgiving, and the habits transfer directly: measure before animating, prefer transforms over layout, and treat dropped frames as bugs — not as the price of looking nice.

If you want to see the implementation, the source for this site is on GitHub, and the motion primitives live in a single file — Reveal.tsx — that the whole site shares.