All writing

We removed jQuery, GSAP and the Webflow runtime. The JavaScript now weighs 20 KB

Every animation on this site was rebuilt in CSS. What the bundle weighs now, which effects were hard, and the three techniques that replaced a library each.

This site used to be a Webflow page: jQuery, the Webflow runtime, GSAP, ScrollTrigger, Flip and SplitType. Rebuilt, the JavaScript is about 20 KB gzipped, and roughly 6 KB of that is a client-side router I chose rather than needed.

Nothing was cut. Every effect on the old site is still here. Three techniques did most of the replacing.

1. Reveals: one observer, one class

The most common effect on any marketing site is “animate when it enters the viewport”. It is also what most people install a library for.

It is one IntersectionObserver for the whole page that adds a class. Everything else is a CSS transition on that class. There is no per-element listener, no scroll handler, no timeline.

The gotcha, and it is a real one: the resting state has to live somewhere. These elements start at opacity zero, which means the page is blank without JavaScript unless something undoes it. The fix is a html:not(.w-mod-js) fallback that restores every initial state. It is the difference between a degraded page and an empty one, and it is the sort of thing that is never noticed until a crawler quietly sees nothing.

2. Scroll-linked motion: scroll-driven animations

Where an effect is genuinely tied to scroll position rather than to entering the viewport, CSS now does it natively:

@supports (animation-timeline: view()) {
  .card {
    view-timeline-name: --card;
    animation: card-in linear both;
    animation-timeline: --card;
    animation-range: entry 0% entry 100%;
  }
}

This runs on the compositor. It keeps moving while the main thread is busy, which a JavaScript scroll handler does not.

One hard-won detail: animation-range is where this goes wrong. For a subject taller than the viewport, entry 0% exit 0% is not what you want and produces something that looks inert; entry 0% entry 100% is the correct range. I convinced myself a parallax was broken before realising I had measured it before the page settled.

3. Staggered text: a delay computed from an index

Text is split into words and characters at build time, not in the browser. Each character carries its index as a custom property, and the stagger is arithmetic:

.char {
  transition: transform 0.38s cubic-bezier(0.65, 0, 0.35, 1);
  transition-delay: calc(var(--i) * 12ms);
}

That replaces SplitType entirely, and it is better than the library version in one respect: the characters exist in the HTML that gets served, so a crawler reads the sentence rather than an empty element waiting for a script.

The one that needed arithmetic rather than a library

The hero carries an infinite orbit of project tiles. The obvious implementation is a JavaScript loop updating positions every frame.

Before writing it I checked whether the motion was a pure phase shift, which is to say whether every tile follows the same path offset in time. It is. So the whole thing is one @keyframes, generated at build time, applied to every tile with a different negative animation-delay. No runtime code at all, and it starts mid-cycle rather than from a standing start.

That is the general shape of most of these wins: five minutes checking whether the motion is actually as complex as it looks, and usually it is not.

What CSS does not replace

I am not going to pretend the libraries are pointless.

A seekable timeline. GSAP gives you an object you can pause, reverse, scrub and jump within. CSS gives you a declaration. Where motion has to respond to application state rather than to scroll or hover, GSAP is still the right tool and reimplementing it is a bad trade.

FLIP transitions between layouts. Measuring an element in two places and animating the difference is genuinely fiddly, and GSAP’s Flip is good at it.

Orchestration across many elements with dependencies. Expressible in CSS, miserable to maintain.

The page transition here runs on the Web Animations API rather than CSS for exactly this reason: Animation.finished is already the promise the router wants back, and there are no timers to leave half-applied if a navigation is interrupted.

One trap with that, which cost me an afternoon: Animation.finished only settles while the document timeline is advancing, and the timeline stops whenever the page is not composited. A router awaiting that promise in a backgrounded tab waits forever. Every wait on an animation needs a ceiling.

Was it worth it

For this site, yes, and the reason is not the bundle size.

It is that there is now nothing between a request and a rendered page except HTML and CSS. The content is in the markup. The animations are declarations. There is no runtime whose failure makes the page disappear, which matters more than it used to, because a meaningful share of readers are now machines that do not run any of it.

The 20 KB is a side effect. The point is that the page works before it arrives.

Questions

Can you replace GSAP with CSS?
For the effects a marketing site actually uses, yes. Scroll-triggered reveals become one IntersectionObserver adding a class. Scroll-linked motion becomes a CSS scroll-driven animation. Staggered text becomes a transition delay computed from an index. What CSS does not give you is a timeline you can seek, pause and reverse from code, which is why GSAP exists.
How much JavaScript does a marketing site need?
This one ships about 20 KB gzipped, and roughly 6 KB of that is a client-side router that is a preference rather than a requirement. A conventional Webflow page loads jQuery plus the Webflow runtime before any of your own code, which is an order of magnitude more before you have written a line.
Is CSS animation better than JavaScript animation?
It is cheaper and it runs on the compositor, so it keeps moving when the main thread is busy. It is also harder to orchestrate. The right split is CSS for anything declarative and JavaScript only where you genuinely need control over time.
What is a scroll-driven animation?
A CSS feature that ties an animation's progress to scroll position rather than to a clock, using animation-timeline. It replaces the most common reason people reach for a scroll library, and where it is unsupported the element simply arrives at its finished state.

Read next

All writing, or see how I build websites.