DEV Community

Cover image for Animations without GSAP: what 111 free components use instead
Florent Cleron
Florent Cleron Subscriber

Posted on

Animations without GSAP: what 111 free components use instead

I'm Florent, a front-end dev. I build Kodza, a free library of UI components in plain HTML, CSS and JS. 111 of them so far: a menu that swings the page open like a trapdoor, cards that stack as you scroll, a carousel you can throw.

Zero dependencies. Not "lightweight". Zero.

The question I get most: "So what do you animate with?" The honest answer is boring. The browser already ships an animation engine, a pinning system and a frame loop. Here is how each one does the job, with code taken from the components (trimmed a little).

1. Long sequences: the Web Animations API

Trapdoor Menu: the page swings down on its hinge and the menu links rise

Press Menu on Trapdoor Menu and the page tips back on its bottom edge. Each link rises from its mask as the edge uncovers it, a hairline draws in above it, the image uncovers. A couple dozen animations, one timeline.

element.animate() gives you keyframes, easing, delay and duration per element. What it doesn't give you is a timeline. The trick: give every animation the same total length with endDelay. They all start together and end together, so you can drive them as one.

// steps: [element, keyframes, { delay, duration, easing }]
const total = Math.max(...steps.map(([, , o]) => o.delay + o.duration));

const sequence = steps.map(([el, frames, o]) =>
  el.animate(frames, { ...o, fill: 'both', endDelay: total - o.delay - o.duration })
);
Enter fullscreen mode Exit fullscreen mode

Closing is the same timeline played backwards, a bit faster:

sequence.forEach((animation) => {
  animation.updatePlaybackRate(-1.6);
  animation.play();
});
await Promise.all(sequence.map((animation) => animation.finished));
Enter fullscreen mode Exit fullscreen mode

Press Menu again halfway through the opening, and it turns around from where it is. No second timeline to keep in sync.

Two details that helped:

  • updatePlaybackRate() rather than setting playbackRate: the speed changes without a jump, even for animations running on the compositor.
  • The pseudoElement option animates ::before and ::after directly. The menu's cross is two pseudo-elements, so no extra spans in the markup:
cross.animate(
  [{ transform: 'rotate(0deg)' }, { transform: 'translateY(3.25px) rotate(45deg)' }],
  { delay: 200, duration: 600, easing: 'cubic-bezier(0.22, 1, 0.36, 1)', pseudoElement: '::before' }
);
Enter fullscreen mode Exit fullscreen mode

2. Pinning: position: sticky

Sticky Cards: each card sticks below the previous one, the covered card steps back

Sticky Cards stacks cards as you scroll. Each one sticks a little below the previous, and the card being covered steps back and darkens.

The stacking is CSS. No pin spacer, no scroll handler holding things in place, nothing to recompute on resize:

.kodza-stack__card {
  position: sticky;
  top: calc(var(--kz-stack-top) + var(--i, 0) * var(--kz-stack-offset));
}
Enter fullscreen mode Exit fullscreen mode

The script sets --i on each card, then measures how much each one is covered to scale and darken it. That's the only job left for JavaScript. If it never runs, the cards still stick and stack.

3. Inertia: a spring in twenty lines

Carousel: the slides glide and settle

Carousel is a native scroll-snap carousel first. On top of it: buttons, and mouse drag you can throw. The glide is a critically damped spring, solved in closed form and sampled in requestAnimationFrame. No physics library.

// From `from` (moving at `velocity` px/s) to `to`
const spring = (from, to, velocity, omega, onFrame, onDone) => {
  const x0 = from - to;
  const b = velocity + omega * x0;
  const start = performance.now();
  let raf = 0;
  const step = (time) => {
    const t = Math.max(0, (time - start) / 1000);
    const decay = Math.exp(-omega * t);
    const x = to + (x0 + b * t) * decay;
    const v = (velocity - omega * b * t) * decay;
    const done = (Math.abs(x - to) < 1 && Math.abs(v) < 30) || t > 3;
    onFrame(done ? to : x);
    if (done) onDone();
    else raf = requestAnimationFrame(step);
  };
  raf = requestAnimationFrame(step);
  return () => cancelAnimationFrame(raf);
};
Enter fullscreen mode Exit fullscreen mode

It's a formula of time, not a step-by-step simulation, so a slow frame doesn't change where it lands. From rest, it eases in and out. Started at the speed of your release, it keeps your hand's momentum, then settles on the slide where the throw would have ended.

4. The part nobody puts in the demo: cleanup

Animating is the easy part. The hard part is a WordPress theme that swaps content with Ajax, a page router, or React mounting everything twice in strict mode.

Every Kodza component mounts the same way: one instance per root, and everything it starts is tied to one AbortSignal.

function mount(root, signal) {
  window.addEventListener('resize', onResize, { signal });
  const observer = new IntersectionObserver(onView);
  observer.observe(root);
  signal.addEventListener('abort', () => observer.disconnect());
}
Enter fullscreen mode Exit fullscreen mode

Two events drive it, the same in every component:

document.dispatchEvent(new CustomEvent('kodza:destroy', { detail: { root: oldContent } }));
// swap the content
document.dispatchEvent(new CustomEvent('kodza:init', { detail: { root: newContent } }));
Enter fullscreen mode Exit fullscreen mode

kodza:destroy aborts the signal (listeners, observers, timers and loops stop) and puts the original markup back. kodza:init starts whatever is inside the new root. That's how the same files run in a static page, a theme with Ajax navigation, Swup or a React app.

And every component reads prefers-reduced-motion before it moves anything. Reduced motion is not a setting, it is a person.

What GSAP still does better

GSAP is excellent and battle-tested, and its plugins (SplitText, MorphSVG, Draggable) solve problems the platform doesn't solve yet. If your project already loads it, keep it.

But for a menu that opens, cards that stack and a carousel you can throw, the platform covers it. One less script to load, version and update, and the component you copy works without a package manager.

Try it

All 111 components are free, with the code on each page: kodza.dev

Your AI assistant can fetch them too. In Claude Code:

claude mcp add -t http kodza https://kodza.dev/api/mcp
Enter fullscreen mode Exit fullscreen mode

Then ask for a component in plain words. Cursor and VS Code work too, setup on kodza.dev/docs.

What's the last animation you reached for a library to do? I'm curious which ones the platform still can't handle.

Top comments (0)