Nov 25, 2025

Introducing the Motion+ Carousel

Matt Perry

Carousels are a cornerstone of web design, but over the years they've proven difficult to get right. Between CSS limitations and clunky JS gestures, building a carousel that feels fluid, scrolls infinitely, looks great, and offers a flexible developer experience is one of the most deceptively complicated challenges in web UX.

Today, we're launching the Motion+ Carousel for React. It's a new component that makes it easy to build performant, accessible, and fully-featured carousels.

A long time coming

The perfect carousel is a personal white whale of mine.

12 years ago, I was working on Prime Video, tasked with implementing a simple carousel of titles. The design team had built a beautiful new implementation, but the stock Amazon carousel was riddled with UX flaws. It was clunky, slow to paginate, and unable to recreate the specced design. So I set about making my own.

I still remember the angry conference call to me and my manager when the core UI team discovered I’d spent a month reinventing their square wheel. From my perspective, it was worth it. If I don't think something is up to scratch, I can't ship it in good conscience.

It seemed like at every subsequent job, I kept running into the same requirement - build a beautiful carousel - and the same dissatisfaction with available solutions. My iterations on this problem culminated in a 2017 tutorial literally titled Create the Perfect Carousel. Although "perfect" was probably an overstatement, many of the techniques I wrote about then have landed here: refined, expanded, and modernised. It's getting closer.

The trouble with carousels

If you've ever tried building a carousel from scratch, or even using an off-the-shelf solution, you've likely run into a wall of some kind.

CSS has come a long way, but even now, with overflow: scroll and scroll-snap, you can still find yourself fighting the browser.

  • How do you have items visually break out of the carousel container?

  • How do you wire up custom controls like pagination buttons?

  • Do you want to settle for poor native wheel handling within scroll snap containers?

Some of these limitations do have "clever" solutions - sort of. But these often invoke the runic,compiler-target aesthetics of deep CSS. And the biggest blocker of all? Infinite scrolling. It simply isn’t possible with CSS alone.

JavaScript libraries promise to solve these issues, but often introduce their own frustrations.

They can suffer from poor gesture implementations, rigid control APIs, massive bundle sizes and more.

One of my biggest pet peeves is when they paginate by item, forcing users to click through lots of items they've already seen.

Sometimes this is even exasperated by pagination buttons that are blocked until the active pagination animation has finished, making it even slower to navigate through.

Enter Motion+ Carousel

This is why built the Motion+ Carousel. For existing Motion users, it's lightweight, at only +6kb.

It's built on the same unique rendering engine as the popular Motion+ Ticker, which minimises or even eliminates item cloning for buttery-smooth infinite scrolling.

But the real magic is in the details. The stuff that we obsess over, that some people notice consciously, and that everyone notices subconsciously.

Pagination

Pagination is a deceptively deep topic, but it’s the bedrock of a flexible carousel. I have two fundamental rules for how pagination should behave.

1. A "page" is the distance until the first obscured item.

Most carousels get this wrong. When a user clicks "next," we can safely assume they’re finished looking at the currently visible items. Animating just one item at a time is inefficient. Animating the full container width often leaves one item that has never been properly shown in its entirety.

Instead, the Motion+ Carousel moves all the fully visible items out of view. This dramatically reduces the time it takes to paginate through a whole shelf and scales gracefully across item sizes and breakpoints.

2. If a new pagination event happens during an animation, the next page is calculated from the animation target, not the current position.

This is the difference between creeping and leaping. Clicking "next" rapidly should get you through the carousel faster, not slower. Three quick clicks will move you forward three full pages - not one.

Page redistribution

You’ve seen it before: you paginate through a carousel and the very last page is a tiny sliver of a single item.

Motion+ Carousel detects these "straggler" pages. If the final page is significantly shorter than the average page length, it attempts to redistribute the pages to create a more balanced pagination.

This is a slightly experimental feature in that this redistribution isn't always possible, but we're interested to hear what you think!

Headless controls

Aside from automatically-applied styles that make a carousel, well, a carousel, Motion+ Carousel is headless. This gives you complete freedom to build custom controls and pagination indicators that match your design system, using your own component libraries.

You can pass your own components as children and use the Motion+ useCarousel hook to access everything you need: nextPage(), prevPage(), gotoPage(index), currentPage, and totalPages.

function Pagination() {
  const { currentPage, totalPages, gotoPage } = useCarousel()

  return (
    <ul className="dots">
      {Array.from({ length: totalPages }, (_, index) => (
        <li className="dot">
          <motion.button
              initial={false}
              animate={{ opacity: currentPage === index ? 1 : 0.5 }}
              onClick={() => gotoPage(index)}
          />
        </li>
      )}
    </ul>
  )
}

Perfecting wheel and touchpad swipe

Getting wheel and touchpad navigation to feel as natural as native scroll-snap turned out to be one of the hardest (and most interesting) parts of this project.

The reason touchpad navigation feels so good with native CSS scrolling is that the browser knows when a user's fingers have left the trackpad, allowing it to "catch" scrolling momentum and snap to a position, but this isn't exposed via any public JavaScript API, so I had to find a different approach.

Attempt 1: The Accumulator

My first attempt was simple: accumulate the wheel delta values, and when they reached a certain threshold, trigger a page swipe.

This model fell apart with modern mice and trackpads that have momentum and acceleration. The accumulator would keep filling up long after the user intended to stop, leading to tons of unwanted pagination events.

Attempt 2: The Scroll Spy

Next, I tried a "scroll spy" technique. I created a hidden, "ghost" carousel filled with empty items that perfectly matched those of the carousel.

The idea was to detect wheel events, then redirect wheel events to this ghost element, measuring its offset and applying this to the underlying element. Thereby capturing native momentum, interruption, and scroll snapping events.

For a prototype, it worked surprisingly well, but I hit a couple blockers.

One, I couldn't get it to work with infinite scrolling. Manually setting the scroll position during a snap would lead to flickering.

The other, harder show stopper, was (of course) Safari. In Chrome, it was possible to activate the scroll spy element with pointer-events: auto after the initial wheel event, but for Safari this moment was already too late to enable scroll.

Attempt 3: The wheelSwipe Gesture

My final attempt, and the ultimate solution, is a new gesture recogniser: wheelSwipe. It carefully monitors wheel deltas to detect when the user's scroll is entering a consistent deceleration phase.

In practise, this kind of deceleration has proven to be a good indicator that the user has stopped scrolling and what we are receiving are simulated wheel events.

From here, if it detects either a change in wheel direction, or a new acceleration, it instantly interrupts the pagination animation to return direct control to the user.

We're still tweaking this gesture, but already the result is a pretty robust, responsive swipe that feels intuitive and native across all devices.

Animate items with scroll

Because the Carousel is built on top of the Ticker component, you get full access to the useTickerItem hook. This hook returns a motion value representing each item's visual offset from the start of the container. You can use this value to drive creative, per-item animations that react to the carousel's scroll position.

function Item() {
  const { offset } = useItemOffset()
  const rotate = useTransform(offset, [0, 300], [0, 360], { clamp: false })

  return (
    <motion.div style={{ rotate }}>
      😂
    </motion.div>
  )
}

The possibilities are endless, from parallax effects to 3D rotations.

How do I get Motion+ Carousel?

This was a whirlwind tour of the new Motion+ Carousel, but not an exhaustive one. We didn't cover the fade prop, which fades overflowing content out using an animated mask-image. Or the overflow prop, which allows carousel content to break out of their scrollable container bounds. But I've given you an idea of the kind of details that we sweat.

Carousel is a premium component available today for React, with a Vue version in development. It's included in Motion+. Motion+ is a one-time fee, lifetime membership to:

  • Premium APIs like Carousel, Ticker, AnimateNumber, and splitText

  • Unlock a vault of 300+ examples and 40+ tutorials.

  • Early access to new content and APIs like AnimateActivity and soon, wheelScroll.

  • A private Discord with over 600 Motion superusers

  • Visual and AI editing tools for your code editor.

I hope to see you in the Discord! I can't wait to see what you build with Carousel.

Motion+

Motion+

Level up your animations with Motion+

Unlock the full vault of 290+ Motion examples, premium APIs, private Discord and GitHub, and powerful VS Code animation editing tools.

One-time payment, lifetime updates.

Read more

Motion's independence: One year in review

It's been an entire year since I took Framer Motion independent as Motion. Here's how it's gone, and what I learned in this first year.

The Web Animation Performance Tier List

Learn what makes web animations fast, slow, and everything in between with our 2025 web animation performance tier list.

Building the ultimate ticker

Ticker compoments are becoming common decorative features, but building a good one isn't easy. Let's take a look at the problem with CSS components, and how Motion+ Ticker aims to solve them.

Introducing magnetic and zoning features in Motion+ Cursor

Motion+ Cursor gains powerful magnetic and zoning features, allowing your cursors to react to, snap to, and morph to hit targets.

Supercharging the GTA VI website with Motion

Rockstar's beautiful GTA VI website could load even faster with Motion's deferred keyframe resolution and use of native browser APIs.

Introducing Motion for Vue

Motion finally arrives on Vue, complete with variants, scroll, layout animations, and everything else you love from Framer Motion.

Revealed: React's experimental animations API

React is experimenting with a new animation API based on the View Transition API. How does it work? What can it do? We reveal all in this blog post.

How to add cmd-k search shortcut to your Framer site

By default, the Framer Search component doesn't support the cmd-k keyboard shortcut. Here's how to add it to your Framer site.

Framer Motion is now independent, introducing Motion

Framer Motion is now independent. Introducing Motion, a new animation library for React and all JavaScript environments. Here's what it means for you.

Do you still need Framer Motion?

In the five years since Framer Motion was released, CSS animation APIs have come a long way. Do you still need to use Framer Motion?

When browsers throttle requestAnimationFrame

In specific situations Safari and Firefox may throttle requestAnimationFrame. Here’s why your JavaScript animations are janky.

Motion is supported by the best in the industry.