Taking your select component to the next level with framer motion

Kenneth Duverge

April 9, 2024

Animating with framer-motion

I've been using framer-motion the past couple of years now and I'm still amazed at how easy it is to implement nice subtle animations. Animations can greatly enhance the user experience and give an app a more polished feel, when done right.

For the purpose of this article we're going to focus on the framer-motion portion of the code. The component itself is built with shadcn/ui, tailwind and radix.

Grab the component code here
// An object so we can show the full album name in the input
const DOOM_ALBUM_MAP = {
  food: 'Mm..Food',
  doomsday: 'Operation Doomsday',
  madvillainy: 'Madvillainy',
};

const DoomSelect = () => {
  const [doomAlbum, setDoomAlbum] = useState('doomsday');
  return (
    <Select onValueChange={setDoomAlbum} value={doomAlbum}>
      <SelectTrigger className="w-[200px] focus:outline-dashed focus:outline-2 focus:outline-offset-2 focus:outline-zinc-100 bg-zinc-700 text-white overflow-hidden">
        // We&apos;ll get to this part 😉
      </SelectTrigger>
      <SelectContent className="bg-zinc-700 text-white">
        <SelectItem value="food">Mm..Food</SelectItem>
        <SelectItem value="doomsday">Operation Doomsday</SelectItem>
        <SelectItem value="madvillainy">Madvillainy</SelectItem>
      </SelectContent>
    </Select>
  );
};

Alright so the animation I had in mind was something like a reverse slot machine. We have the old value slide up from the center and out while the new value slides up from the bottom and in.

Slot machine SVG

motion

We'll start off with importing motion from framer-motion. This component gives us access to all of the DOM elements (ie. motion.div, motion.a etc) with top level motion props. A span will be good for our use case so let's go with that.

Let's break down each line.

import { motion } from 'framer-motion';

<motion.span
  initial={{ y: '100%', opacity: 0 }}
  animate={{ y: 0, opacity: 1 }}
  exit={{ y: '-100%', opacity: 0 }}
>
  {children}
</motion.span>

initial

 initial={{ y: '100%', opacity: 0 }}

This is the starting point of the animation. Since we want the value to slide in from the bottom we'll set its y axis to 100% ( height of the content ), and to add a subtle fade-in animation we'll set the opacity to 0.

animate

 animate={{ y: 0, opacity: 1 }}

We've told framer-motion where the animation should start from now we need to tell it where it should animate to. That's where the animate prop comes in. By setting y to 0 and opacity to 1 we bring the text back into the middle of the input box.

Animation happens on page load but not after selecting another option

So far so good! Just two props and we're already half way there.

exit

 exit={{ y: '-100%', opacity: 0 }}

To get that slide out slot machine effect we need to specify the exit animation. Set y to negative 100% to go ⬆️ and fade it out with opacity 0.

Now if you try selecting an item from the dropdown you'll notice the exit animation is still not working as we'd expect. The old value exits right away.

Why is that?

Well if we think about it we're trying to keep one value around while the new value animates in. React by itself doesn't know how keep an element in the DOM till after it's exit Animation, it removes it instantly. So how are we going to keep two elements around long enough for their animations to run?

A few things:

  1. We need to tell framer-motion, "Hey, run the exit animation of the old value first and then animate the new value in".
  2. We need to tell React to remount this component whenever the value changes

AnimatePresence

AnimatePresence is a provider component that allows to animate components before they're removed from the tree. This is directly from the documentation.

AnimatePresence allows components to animate out when they're removed from the React tree.

All we need to do is wrap our motion component in AnimatePresence and pass in the mode=wait prop which tells framer-motion to run the exit animation of the node being removed first.

Putting it altogether we should now have something like this.

const AnimatedValue = ({ children }) => {
  return (
    <motion.span
      initial={{ y: '100%', opacity: 0 }}
      animate={{ y: 0, opacity: 1 }}
      exit={{ y: '-100%', opacity: 0 }}
    >
      {children}
    </motion.span>
  );
};
const DoomSelect = () => {
  const [doomAlbum, setDoomAlbum] = useState('doomsday');
  return (
    <Select onValueChange={setDoomAlbum} value={doomAlbum}>
      <SelectTrigger className="...">
        <AnimatePresence mode="wait">
          <AnimatedValue>
            {DOOM_ALBUM_MAP[doomAlbum]}
          </AnimatedValue>
        </AnimatePresence>
      </SelectTrigger>
      <SelectContent className="...">
        <SelectItem value="food">Mm..Food</SelectItem>
        <SelectItem value="doomsday">Operation Doomsday</SelectItem>
        <SelectItem value="madvillainy">Madvillainy</SelectItem>
      </SelectContent>
    </Select>
  );
};

React's key prop

A neat feature about the key prop is that we can use it to tell explicitly tell React to re-render a component. We'll need to specify a unique key, fortunately for us we can use the doomAlbum state variable.

const DoomSelect = () => {
  const [doomAlbum, setDoomAlbum] = useState('doomsday');
  return (
    <Select onValueChange={setDoomAlbum} value={doomAlbum}>
      <SelectTrigger className="...">
        <AnimatePresence mode="wait">
          <AnimatedValue key={doomAlbum}>
            {DOOM_ALBUM_MAP[doomAlbum]}
          </AnimatedValue>
        </AnimatePresence>
      </SelectTrigger>
      <SelectContent className="...">
        <SelectItem value="food">Mm..Food</SelectItem>
        <SelectItem value="doomsday">Operation Doomsday</SelectItem>
        <SelectItem value="madvillainy">Madvillainy</SelectItem>
      </SelectContent>
    </Select>
  );
};

Now the animation is working as expected 🎉

"Used to wear flip flops now rare gear coppers"

Conclusion

Let's recap what we've done:

  • framer-motion gives us a motion component that extends all HTML elements.
  • To define how we want our component to animate from one point to another we can use the initial & animate prop.
  • If we want to give a component an exit animation we need to specify the exit prop and wrap our component in AnimatePresence with mode set to wait.
  • Finally, we need to set the key prop to kick off the initial animation for the new value.

This of course is a simple animation but you can do so much more with framer-motion. I encourage you to check out their documentation and examples of cool stuff people have built.

Resources

If you found this article interesting and want to know when the next one comes out consider dropping your email below.

home.blog.projects.playground.