Smooth scroll animations with CSS and no JS library

I picked up a ticket last week for a high-end WooCommerce store whose homepage carousel was wrecking their Core Web Vitals. They were running one of those heavy JS slider libraries, the kind that ships about 150KB of scripts to move three images. On a high-traffic site that is fatal for performance. Scroll event listeners had the main thread choked, and mobile users got a stuttering mess.

My first thought was to optimize the JS, or swap it for a lighter Intersection Observer script. I spent about three hours building a custom observer, and it mostly worked. Then I hit a weird lag on low-end Android devices: the JS could not keep pace with the scroll without dropping frames. I was overcomplicating it. The browser can run scroll-based animations natively, so the fix was to stop writing JS for it.

What the view() function does

The CSS animation-timeline property, paired with the view() function, ties an animation’s progress directly to how visible an element is inside the scrollport. That means no observers, no scroll listeners, and the movement runs on the GPU. It is defined in the MDN view() specification, and it is what I used on that furniture site.

Same reasoning as managing Gutenberg typography: cleaner code, fewer bloated plugins. This is the structure I used for the carousel slides.

.bbioon-carousel {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
}

.bbioon-slide {
  flex-shrink: 0;
  width: 33.333%;
  scroll-snap-align: center;
  /* Attach the animation to the view timeline */
  animation: bbioon-reveal linear both;
  animation-timeline: view(inline);
}

@keyframes bbioon-reveal {
  0%, 100% {
    transform: scale(0.8);
    opacity: 0.5;
    filter: blur(4px);
  }
  50% {
    transform: scale(1);
    opacity: 1;
    filter: blur(0);
  }
}

Fine-tuning with animation-range

One thing I learned the hard way: leave animation-range undefined and the animation can start far too early or finish before the user ever sees it. I ran with the default at first, and the slides were already half-faded by the time they reached the center. Be specific instead. animation-range: entry 10% exit 90%; puts the effect where the reader is actually looking.

This beats the older approaches on cost. Plenty of people are still prototyping with AI tools that spit out messy jQuery, when native CSS is the thing to aim for. Check the current browser support for scroll-driven animations against your client’s audience first, though on most modern setups it is safe to ship.

Stop fighting the browser

What I took from that furniture site project: do not force JavaScript to do a layout engine’s job. Switching to animation-timeline: view() cut the homepage script size by 60% and moved their LCP (Largest Contentful Paint) back into the green. The win came from taking work away, not from adding a feature.

The idea is simple. Instead of running an animation on a clock, you run it on a scroll position. CSS-Tricks goes further into the differences between the scroll() and view() functions. For per-item reveals in a carousel or a list, view() is the one you want.

This gets complicated fast once you are in complex WooCommerce templates or a legacy theme. If you are tired of debugging someone else’s mess and you just want the site to be fast, drop me a line. I have probably seen it before.

So are you still driving your scroll effects with JS, or are you ready to hand that job to the browser?

author avatar
Ahmad Wael
I'm a WordPress and WooCommerce developer with 15+ years of experience building custom e-commerce solutions and plugins. I specialize in PHP development, following WordPress coding standards to deliver clean, maintainable code. Currently, I'm exploring AI and e-commerce by building multi-agent systems and SaaS products that integrate technologies like Google Gemini API with WordPress platforms, approaching every project with a commitment to performance, security, and exceptional user experience.