Animating a list in React with Framer Motion

Animating a list in React with Framer Motion

Learn list item stagger animation in React by re-creating the iOS Control Centre Focus Modes animation using Framer Motion

Introduction

Apple iOS is full of nifty animations and a great source of inspiration when thinking about motion in your next web app. Many of these take users from one view to another in a seamless transition instead of a jarring switch.

One such example is on clicking the Focus mode selection button in the Control Centre:

In this article, we'll re-build this animation in React with Framer Motion.

In case, you're not familiar with Framer Motion, I recommend checking out the article:

which goes over some basics of Framer Motion by creating a reusable animated check icon component.

Demo

Checkout the code and demo of the final animation we'll build as part of this article:

Layout Animation

After playing around with the demo above, did you notice how clicking on the Focus button takes you to the list of focus mode options in a smooth transition? This is a result of the Focus button "moving into" the list of options and becoming the Do Not Disturb button.

Wait, doesn't that require a lot of complex logic to make sure the animation happens performantly? It's the reason the FLIP technique exists, but we want to keep our code declarative. That's where the layout prop provided by Framer Motion comes into play.

You start by adding the layout prop to a motion component, in this case, the motion button:

<motion.button layout>Focus</motion.button>

Now, anytime a style change happens that affects this button's layout, Framer Motion will transition between the layout changes automatically in a performant way. Try clicking the Toggle layout button in the demo sandbox below and see it smoothly transition between two different locations. Then, try removing the layout prop from the button and see the difference!

By default, the layout prop animates any changes to the size and position of the motion component it's used on. This can, at times, lead to undesirable animations where the content can be squished or stretched when transitioning between layouts, especially when the size changes between the start and end state.

To circumvent this, the layout prop also accepts one of the following values:

  • layout="size" only animates the size changes in the motion component, for instance, the width and height.

  • layout="position" only animates changes in the position of the motion component, as in our example.

  • layout="preserve-aspect" only animates the size & position if the aspect ratio remains the same between the transition, and just position if the ratio changes.

In our use case, where we only want the position of the Focus button to transition to the Do Not Disturb button, we'll make use of the layout="position" prop.

Now that we know we only want the layout position to change and how it works, how do we create the transition between the two buttons? After all, we don't have a pre-calculated value to know where the Do Not Disturb button should render on the DOM. We want to be able to render the focus modes list component anywhere in the app while having the animation work as desired.

Well, Framer Motion provides yet another prop called layoutId to create shared layout animations.

View the sandbox below to see how two different motion buttons are rendered separately in the DOM with their styling, but by providing the same layoutId to both of them, Framer Motion can transition between the two as they are removed or rendered in the DOM:

Variants & Stagger Animation

As we saw in the article about creating an animated check icon, we can provide an initial and animate prop to a motion component to specify the states it should start with and end at, and Framer Motion automatically animates the transition between the states. In that article, we passed an object to these props specifying the styles for the two states but the initial and animate can also accept a string which should correspond to the name of a Framer Motion Variant.

A variant is an object with the variant name for the key and the animation properties for the values. They also prove to be a nice way to be more descriptive about animation states in Framer Motion.

For instance, here's the variant object defined for the focus modes list items:

{
  hidden: {
    y: -10,
    opacity: 0
  },
  visible: {
    y: 0,
    opacity: 1
  }
}

If we break this down, we see that:

  • When the variant name is hidden, the element's opacity is 0 and it sits 10 pixels above its normal position

  • However, when the variant name is visible, the element's opacity is 1 and it sits at its normal position

We can take this variant object and provide it to a motion li component which we will render for each focus mode:

<motion.li
  variants={{
    hidden: {
      y: -10,
      opacity: 0
    },
    visible: {
      y: 0,
      opacity: 1
    }
  }}
>
  Do Not Disturb
</motion.li>

Alright, now that we've specified what styles we want for our list items corresponding to variant names hidden and visible, we need a way to determine when a variant gets applied.

Now's a good time to mention that variants propagate from parent components to their children. This means if we set these variant names to the parent ul to be applied for the initial and animate props, these would automatically flow down to the child li. So, as soon as the ul mounts, it's initially using the hidden variant and animates to the visible variant, which is the same behaviour its children li follow:

<motion.ul
  initial="hidden"
  animate="visible"
>
  <li variants={variantsFromTheSnippetAbove}>Do Not Disturb</li>
</motion.ul>

Not only are variants helpful to keep our animation states more readable and have the values propagate to children, but they also have another trick up their sleeves. Variants are helpful to orchestrate animations! This means we can set additional animation configurations on the parent element's variants prop to define the animation execution of its child motion components. This will become clearer by explaining the snippet below:

<motion.ul
  initial="hidden"
  animate="visible"
  variants={{
    visible: {
      transition: {
        delayChildren: 0.2,
        staggerChildren: 0.05
      }
    }
  }}
>
  <li variants={variantsFromTheSnippetAbove}>Do Not Disturb</li>
</motion.ul>

We've added a new variants prop to the ul which defines a transition behaviour for the visible variant. It says that whenever the visible variant is applied, please delay the animation of the children by 0.2 seconds and stagger them with a delay of 0.05 seconds, so they render one by one, as it does on iOS.

Play around with the code sandbox below to view how variants help us create the stagger animation we want for our focus modes list:

Putting It All Together

It's time to put the layout animation along with the variant stagger animation we've learnt to re-create the iOS animation.

We'll create the Focus button with the layout="position" prop and give it a layoutId of focusModeButton. This button's onClick event handler will update the state to display the focus modes list we'll create next.

<motion.button
  layout="position"
  layoutId="focusModeButton"
  onClick={() => setIsFocusModesListVisible(true)}
>
  Focus
</motion.button>

To create the focus modes list, we'll use the ul and li elements along with Framer Motion's variants prop as we saw in the last section.

We can define the variants for the list container and the list items in separate constant objects as follows:

const LIST_CONTAINER_VARIANTS = {
  visible: {
    transition: {
      delayChildren: 0.2,
      staggerChildren: 0.05
    }
  }
};

const LIST_ITEM_VARIANTS = {
  hidden: {
    y: -10,
    opacity: 0
  },
  visible: {
    y: 0,
    opacity: 1
  }
};

These are the same animation configurations we saw earlier to re-create the focus modes list stagger animation. Finally, we'll apply these variants to the ul and li elements, while providing the first Do Not Disturb button, the same layout animation behaviour as we did for the Focus button, i.e., layout="position" and the same layoutId of focusModeButton:

<motion.ul
  variants={LIST_CONTAINER_VARIANTS}
  initial="hidden"
  animate="visible"
>
  <motion.li>
    <motion.button
      layout="position"
      layoutId="focusModeButton"
    >
      Do Not Disturb
    </motion.button>
  </motion.li>

  <motion.li variants={LIST_ITEM_VARIANTS}>
    <motion.button>Work</motion.button>
  </motion.li>

  <motion.li variants={LIST_ITEM_VARIANTS}>
    <motion.button>Sleep</motion.button>
  </motion.li>

  <motion.li variants={LIST_ITEM_VARIANTS}>
    <motion.button>Personal</motion.button>
  </motion.li>
</motion.ul>

Using these two components, we can use React's state to toggle between the Focus mode button and the focus modes list, while Framer Motion handles the animation between the two:

{isFocusModesListVisible ? (
  <FocusModesList onClose={() => setIsFocusModesListVisible(false)} />
) : (
  <motion.button
    layout="position"
    layoutId="focusModeButton"
    className="FocusModeOpenButton"
    onClick={() => setIsFocusModesListVisible(true)}
  >
    Focus
  </motion.button>
)}

With this, we end up with the final animation as we set out to build:

Thanks for reading and stay tuned for more articles on the topic!