Scroll-Driven Animations Without Libraries
JavaScript animation libraries have dominated the web for years. GSAP, Framer Motion, Anime.js — they’re powerful, well-tested, and widely adopted. But for my latest project, I wanted to see how far I could push scroll-driven animations using only CSS and a thin layer of vanilla JavaScript. The results were better than I expected.
The Scroll-Driven Paradigm
Traditional scroll animations work by listening to scroll events, calculating positions, and imperatively updating styles. This approach has a fundamental performance problem: scroll events fire on the main thread, and any layout calculations you trigger create a tight feedback loop that can cause jank.
The modern alternative is to decouple scroll tracking from style updates. CSS scroll-timeline and view-timeline APIs let you define animations that progress based on scroll position, entirely handled by the browser’s compositor thread. But browser support is still incomplete, so I took a hybrid approach.
The Architecture
My solution has three layers. First, a CSS-first foundation: all visual transitions — opacity, transforms, color changes — are defined in CSS with transition properties. No inline style manipulation for visual effects. Second, a scroll observer: a single IntersectionObserver watches sentinel elements and toggles CSS classes. This is the only JavaScript involved in triggering animations. Third, scroll-linked transforms: for the project showcase where images need to track scroll position precisely, I use a single requestAnimationFrame loop that reads getBoundingClientRect and updates a CSS custom property.
The key insight is that requestAnimationFrame combined with CSS custom properties gives you the precision of JavaScript with the performance of CSS transitions. You calculate a value in JS, set it as a custom property, and let CSS handle the interpolation.
.image-track {
transform: translateY(calc(var(--scroll-offset) * 1px));
transition: transform 0.15s ease-out;
}
The transition on the transform property smooths out any discontinuities from the RAF loop, giving you buttery-smooth animation at a fraction of the cost.
Lessons Learned
The biggest lesson was restraint. When you have a library like GSAP available, it’s tempting to animate everything. Without that crutch, every animation had to justify its existence. Does this transition help the user understand the interface? Does this motion convey hierarchy or state change? If the answer was no, the animation got cut.
I also learned that perceived performance matters more than actual frame rates. A well-timed 200ms opacity fade feels smoother than a perfectly 60fps parallax effect that adds visual noise. The browser’s built-in easing functions — particularly ease-out for entrances and ease-in for exits — look better than most custom cubic-bezier curves I’ve written.
The constraint of working without libraries didn’t limit what I could build. It clarified what I should build. And the resulting code is more maintainable, more performant, and frankly more understandable than any animation library setup I’ve worked with.