Articles

Building Animations Without Libraries

Published on

I’ve been using GSAP for years because honestly it’s great and the best option out there. But lately I’ve been questioning whether I actually need it anymore.

Modern CSS has gotten really good. The linear() easing function especially, it lets you create custom physics-based animations without pulling in an entire library. I started experimenting with a CSS-first approach a few months ago and I’m not sure I can go back.

Why I’m doing this

Look, GSAP is great. But every project doesn’t need 50kb+ of animation libraries. I kept noticing my sites felt heavier than they should be, and when I dug into it, a huge chunk was just animation dependencies.

So I built a system that handles 90% of what I normally do with GSAP, but it’s mostly just CSS and a tiny bit of vanilla JS. It’s declarative, meaning you set your intent in the HTML, the logic lives in CSS, and everything stays organized.

Ready to ditch GSAP?

I’ve been using GSAP for years because honestly, its great and the best option out there for complex animations (still). But lately I’ve been questioning whether I actually need it anymore.

Modern CSS has gotten really good. The linear() easing function lets you create custom physics-based animations without pulling in an entire library (we all love that). I started experimenting with a CSS-first approach a few months ago and I’m not sure I can go back.

The setup

:root {
  --expo-out: cubic-bezier(0.16, 1, 0.3, 1);
  --back-out: cubic-bezier(0.34, 1.56, 0.64, 1);
  --power4-out: cubic-bezier(0.25, 1, 0.5, 1);
  
  /* This is the cool part - bounce physics in pure CSS */
  --bounce: linear(0, 0.0039, 0.0157, 0.0352, 0.0625, 0.0977, 0.1407, 0.1914, 0.2499, 0.3164, 0.3906, 0.5625, 0.7656, 1, 0.8906, 0.8125, 0.7656, 0.75, 0.7656, 0.8125, 0.8906, 1);
}

Then I have a base class that handles the animation setup:

.animate-in {
  opacity: 0;
  --duration: 1s;
  --easing: var(--expo-out);
  --base-delay: 0s;
  --stagger: 0.1s;
  --i: 0; 
  --start-x: 0;
  --start-y: 5vh;
  --start-scale: 1;

  animation-delay: calc(var(--base-delay) + (var(--i) * var(--stagger)));
}

.animate-in.visible {
  animation: fadeUp var(--duration) var(--easing) both;
  animation-delay: inherit;
}

@keyframes fadeUp {
  from {
    opacity: 0;
    translate: var(--start-x) var(--start-y);
    scale: var(--start-scale);
  }
  to {
    opacity: 1;
    translate: 0 0;
    scale: 1;
  }
}

How it works in practice

The HTML is super clean. You just add data attributes for what you want to customize:

<div id="hero">
  <h1 class="animate-in" data-y="5vh" style="--i: 1">Strategic Growth</h1>
  <p class="animate-in" data-y="10vh" style="--i: 2">Modern solutions for the modern web.</p>
  <button class="animate-in" data-scale="0.8" style="--i: 3">Get Started</button>
</div>

That --i variable creates the stagger effect. Each element animates in sequence without me having to manually calculate delays.

The JavaScript part

I use IntersectionObserver to trigger animations when elements scroll into view. It’s way more performant than scroll listeners:

const animationObserver = new IntersectionObserver((entries) => {
  entries.forEach(el => {
    if (el.isIntersecting) {
      const target = el.target
      const d = target.dataset

      if (d.duration) target.style.setProperty('--duration', d.duration)
      if (d.x) target.style.setProperty('--start-x', d.x)
      if (d.y) target.style.setProperty('--start-y', d.y)
      if (d.scale) target.style.setProperty('--start-scale', d.scale)
      if (d.easing) target.style.setProperty('--easing', `var(--${d.easing})`)

      target.classList.add('visible')
      animationObserver.unobserve(target)
    }
  })
}, { threshold: 0.1 })

document.querySelectorAll('.animate-in').forEach(ob => animationObserver.observe(ob))

That’s it. The whole thing is just 19 lines of JavaScript.

What about complex animations?

For most projects, the built-in cubic-bezier curves are plenty. But when I need something suuper specific like a realistic spring or bounce, I’ll use one of the following CSS easing generators to generate the linear() values.

I just copy the output into my CSS variables and I’m done.

The tradeoffs

This approach isn’t perfect. If you need timeline-based sequencing or really complex choreography, GSAP is still better. Same if you’re animating SVG paths or doing morphing (there’s a reason the library exists).

But for 90% of web animation work? Fade-ins, slides, staggers, scroll-triggered reveals? I don’t think I need a library anymore. The native stuff is fast, the bundle size is tiny, and maintaining it is way easier when everything’s just HTML and CSS.

Phil

This article was written by Phil,
WordPress Experte in Frankfurt