Carousel: Part 1

Carousel: Part 1

Learn how to use the `x` shorthand property to smoothly animate an image container.

Summary

To animate our image carousel, we'll start by rendering all our images in a flex container:

<div className="flex">
  {images.map((image, i) => (
    <img key={image} src={image} className="aspect-[3/2] object-cover" />
  ))}
</div>

Now that the images are all rendered side-by-side in a row, we can animate them by shifting the flex container left and right. Framer Motion provides a convenience property x which we can use to easiliy set the translateX position of a motion element.

Since each image takes up the full width, translating the container by increments of 100% will animate in each successive image:

<motion.div animate={{ x: `-${index * 100}%` }} className="flex">
  {images.map((image, i) => (
    <img key={image} src={image} className="aspect-[3/2] object-cover" />
  ))}
</motion.div>

If we look at the inspector, we'll see that the shorthand { x: "100%" } is applying the style transform: translateX(100%) to our motion element.

Notice how we're able to derive our animation entirely from our existing React state – the current index. Typically, if you're coding your Framer Motion animations correctly, you should not need to add any new state just for the sake of the animations, and instead be able to derive your animation code from your existing application logic.

Custom easing

Our animation is working, but the default spring animation is a bit aggressive. Let's customize it with the transition prop:

<motion.div
  animate={{ x: `-${index * 100}%` }}
  transition={{ duration: 0.7, ease: [0.32, 0.72, 0, 1] }}
/>

The ease property has some pre-defined values like easeOut, but it also accepts a array of four values that define a cubic bezier curve. There's lots of curves you can find around the internet, but here we're using the one from Ionic's modal enter animation which emulates an iOS transition. This curve turns out to be a perfect fit for our carousel, and adds a nice amount of character while still feeling snappy and responsive.

Avoiding conflicts with CSS transitions

We're hiding our left and right nav buttons when we're at the beginning and end of our carousel. Let's add some animation to these buttons using AnimatePresence.

<AnimatePresence initial={false}>
  {index > 0 && (
    <motion.button
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
      className="bg-white/60 transition hover:bg-white/80"
      onClick={() => setIndex(index - 1)}
    >
      <ChevronLeftIcon />
    </motion.button>
  )}
</AnimatePresence>

Once we add this, we see some jankiness in our animation. It turns out our existing CSS transitions that are being applied with Tailwind's transition class are interfering with our Framer Motion animations.

When adding Framer Motion to animate a specific CSS property, it's usually best to remove any CSS code that is also animating or transitioning that property, so that Framer Motion has total control over the effect

Let's just set the background to pure white, and remove the background opacity and transition from our class list. We can also add back in the hover effect using Framer Motion's whileHover prop:

<AnimatePresence initial={false}>
  {index > 0 && (
    <motion.button
      initial={{ opacity: 0 }}
      animate={{ opacity: 0.7 }}
      exit={{ opacity: 0 }}
      whileHover={{ opacity: 1 }}
      className="bg-white"
      onClick={() => setIndex(index - 1)}
    >
      <ChevronLeftIcon />
    </motion.button>
  )}
</AnimatePresence>

Now there's no more flickering due to CSS transitions interfering with our Framer Motion animations.

Using MotionConfig to reduce duplication

We're currently animating two elements – our container with the x property, and our buttons with the opacity property. In this particular example it'd be ideal if these properties were all animated using the same transition, so that the fades and effects all line up.

Our carousel is shifting using a custom ease transition:

<motion.div
  animate={{ x: `-${index * 100}%` }}
  transition={{ duration: 0.7, ease: [0.32, 0.72, 0, 1] }}
/>

We could copy and paste this transition to our fading button elements:

<motion.button
  initial={{ opacity: 0 }}
  animate={{ opacity: 0.7 }}
  exit={{ opacity: 0 }}
  transition={{ duration: 0.7, ease: [0.32, 0.72, 0, 1] }}
  whileHover={{ opacity: 1 }}
  className="bg-white"
  onClick={() => setIndex(index - 1)}
>
  <ChevronLeftIcon />
</motion.button>

But this duplication could fall out of sync pretty quickly. Fortunately, Framer Motion exports a MotionConfig provider component that we can use to customize the transition for an entire section of our React tree.

To use it, let's improt MotionConfig and render it as a provider above all our common motion elements, passing in our custom transition:

import { MotionConfig, motion } from "framer-motion";

export default function Page() {
  let [index, setIndex] = useState(0);

  return (
    <MotionConfig
      transition={{
        duration: 0.7,
        ease: [0.32, 0.72, 0, 1],
      }}
    >
      <motion.div animate={{ x: `-${index * 100}%` }} className="flex">
        {/* ... */}
      </motion.div>

      <AnimatePresence>
        <motion.button
          initial={{ opacity: 0 }}
          animate={{ opacity: 0.6 }}
          exit={{ opacity: 0 }}
        >
          {/* ... */}
        </motion.button>
        )}
      </AnimatePresence>
    </MotionConfig>
  );
}

Now both our flex container and our buttons share the same custom transition from the provider. This is a great way to keep all current and future animations we add to this component perfectly in sync.

Buy Framer Motion Recipes

Buy the course

$199
one-time payment

Get everything in Framer Motion Recipes.

  • 3+ hours of video
  • 8 lessons
  • Private Discord
  • Summaries with code
  • Unlimited access to course materials

Lifetime membership

$349
Access all coursesone-time payment

Get lifetime access to every Build UI course, including Framer Motion Recipes, forever.

  • Access to all five Build UI courses
  • Full access to all future Build UI courses
  • Summaries with code
  • Video downloads
  • Working code demos
  • Private Discord

What's included

Stream or download every video

Watch every lesson directly on Build UI, or download them to watch offline at any time.

Live code demos

Access to a live demo of each lesson that runs directly in your browser.

Private Discord

Chat with Sam, Ryan and other Build UI members about the lessons – or anything else you're working on – in our private server.

Video summaries with code snippets

Quickly reference a lesson's material with text summaries and copyable code snippets.

Source code

Each lesson comes with a GitHub repo that includes a diff of the source code.

Invoices and receipts

Get reimbursed from your employer for becoming a better coder!