About

Heavy animation and good Core Web Vitals are not mutually exclusive.

That is the claim this site exists to test. Five fictional-brand landing pages, each built around a different animation discipline, each holding a performance budget that is checked mechanically rather than hoped for. The brands do not exist. The engineering does.

The rules

Five things that are never traded away

  1. 01

    Content is server-rendered HTML

    Every heading, paragraph and caption is in the server response. Animation reveals content; it never contains it. Nothing indexable lives inside a canvas or behind a client-only import. Disable JavaScript and every page is still a complete document.

  2. 02

    Reduced motion is a hard requirement

    Under prefers-reduced-motion: reduce, every page presents its complete, final, readable state with no scroll-linked and no automatic movement. Not a degraded page — an equivalent one. Scenes branch at mount rather than cancelling animations after they start.

  3. 03

    Transform and opacity only

    Nothing animates width, height, top, left, margin or padding. No preemptive will-change. Everything that moves during a scroll runs on the compositor.

  4. 04

    Capability decides, not screen size

    A phone runs the same scenes a laptop does — the image sequence against a half-size frame set, the WebGL scene at a device pixel ratio capped to 1.5. What switches them off is a device that says it cannot cope: save-data, a 2G connection, too few cores. On those the sequence becomes a static poster and the WebGL scene never mounts, and not one frame, model or HDRI byte is requested. That is a runtime decision from real device signals, not a media query.

  5. 05

    The budget is enforced, not intended

    Lighthouse runs on every pull request against a throttled mid-range Android profile, and a breached budget fails the build. When a technique cannot hit its budget, the technique gets cut rather than the budget raised.

Stack

What it is built with

Framework
Next.js 16, App Router, every route static
Styling
Tailwind v4, CSS-first tokens, five theme blocks
Scroll and scrub
GSAP + ScrollTrigger, one ticker shared with Lenis
Component motion
Motion, imported per route rather than globally
3D
React Three Fiber, frameloop="demand"
Vector
DrawSVG, MorphSVG, dotLottie

Honestly

What was cut, and why

A portfolio that only lists what worked is not worth reading. Every trade-off on this project is written down as it is taken, with the numbers that forced it.

  • The nav no longer hides on scroll

    It was built with Motion, which put the library in the shared bundle of every route: 41 KB of JavaScript and roughly 150 ms of LCP, for a nicety on the chrome. Three routes went over budget on pages containing nothing but text. Cut.

  • Video scrubbing, then WebCodecs

    Seeking a video element to a non-keyframe forces the decoder back to the previous keyframe, so it stutters; forcing keyframes fixes it at roughly five times the file size. WebCodecs is frame-exact and genuinely the future, but it is still a working draft and needs a demuxer. An image sequence drawn to canvas is what ships.

The full accounting — every route’s field numbers, and what they cost — lands when the seventeen landing pages do. Sources and licences for everything on the site are on the colophon.

Who built it

Adesh Shukla, a front-end engineer. The rest of the work, and the things that are not concept pieces, are at devstash.me.