Multistep wizard

Multistep wizard

Learn the basics of state-based animation with Framer Motion.

Video resources

Summary

To animate an element with Framer Motion, first turn that element into a motion element:

+ import { motion } from "framer-motion";

  function Step({ step }) {
    return (
-     <div>
-+     <motion.div>
+        {step}
-     </div>
+     </motion.div>
    );
  }

Motion elements are a superset of React elements – they support all the same props and behavior as normal elements, but add some additional props related to animation.

The first prop we'll look at is animate, which takes an object whose keys are any CSS property:

<motion.div animate={{ opacity: 0.5 }}>{step}</motion.div>

If you set the values using React state, that property will animate between the values whenever the state changes!

<motion.div
  animate={{
    opacity: status === "complete" ? 0 : 1,
  }}
>
  {step}
</motion.div>

In our case, we want to animate the background color to blue when a step is complete:

<motion.div
  animate={{
    backgroundColor: status === "complete" ? "#3b82f6" : "#ffffff",
  }}
>
  {step}
</motion.div>

We happen to be using Tailwind on this project, which uses CSS classes for our color values – #eb82f6 is our text-blue-500 color. Framer Motion can't animate between CSS classes, but it can animate between CSS variables. So, we've added a Tailwind plugin to output all our color values into CSS variables that can be used in our animation code. Check the source code for this video below to see the plugin.

That means we can rewrite our animation like this:

<motion.div
  animate={{
    backgroundColor:
      status === "complete" ? "var(--blue-500)" : "var(--white)",
  }}
>
  {step}
</motion.div>

Much easier to read and change!

Let's finish by moving the border and text colors from our classes into animate:

<motion.div
  animate={{
    backgroundColor:
      status === "complete" ? "var(--blue-500)" : "var(--white)",
    borderColor:
      status === "complete" || status === "active"
        ? "var(--blue-500)"
        : "var(--slate-200)",
    color:
      status === "complete" || status === "active"
        ? "var(--blue-500)"
        : "var(--slate-400)",
  }}
>
  {step}
</motion.div>

Now, all the properties that are changing with each state change are animating. Nice!

Variants

Our code works, but it's gotten a bit complex. Can you easily tell what happens when a step goes from "incomplete" to "active"?

Fortunately, Framer Motion comes with another tool for writing state-based animations called variants that will clean this code right up.

Variants let us group together all the CSS properties and values that relate to a specific state, and nest them under a single key. They look like this:

<motion.div
  variants={{
    inactive: {
      backgroundColor: "var(--white)",
      borderColor: "var(--slate-200)",
      color: "var(--slate-400)",
    },
    active: {
      backgroundColor: "var(--white)",
      borderColor: "var(--blue-500)",
      color: "var(--blue-500)",
    },
    complete: {
      backgroundColor: "var(--blue-500)",
      borderColor: "var(--blue-500)",
      color: "var(--blue-500)",
    },
  }}
>
  {step}
</motion.div>

To break that down, we start by passing an object into the variants prop, and add a key for each state of our Step component:

<motion.div
  variants={{
    inactive: {},
    active: {},
    complete: {},
  }}
>
  {step}
</motion.div>

Then we add the CSS properties and values for each state:

<motion.div
  variants={{
    inactive: {
      backgroundColor: "var(--white)",
      borderColor: "var(--slate-200)",
      color: "var(--slate-400)",
    },
    active: {
      backgroundColor: "var(--white)",
      borderColor: "var(--blue-500)",
      color: "var(--blue-500)",
    },
    complete: {
      backgroundColor: "var(--blue-500)",
      borderColor: "var(--blue-500)",
      color: "var(--blue-500)",
    },
  }}
>
  {step}
</motion.div>

Now, to actually animate our motion element between the variants we've defined, we need to pass in a string for the current state into animate. We already have a status variable which changes between inactive, active, and complete, so we can use that:

<motion.div
  animate={status} // pass in our React state!
  variants={{
    inactive: {
      backgroundColor: "var(--white)",
      borderColor: "var(--slate-200)",
      color: "var(--slate-400)",
    },
    active: {
      backgroundColor: "var(--white)",
      borderColor: "var(--blue-500)",
      color: "var(--blue-500)",
    },
    complete: {
      backgroundColor: "var(--blue-500)",
      borderColor: "var(--blue-500)",
      color: "var(--blue-500)",
    },
  }}
>
  {step}
</motion.div>

Now every time our state changes, Framer Motion will apply the associated variant's styles! Our steps animate just like before, but our code has become way more readable.

Animating SVG paths

Framer Motion also works on SVG elements. Given our check mark icon

<svg>
  <path
    d="M5 13l4 4L19 7"
    strokeLinecap="round"
    strokeLinejoin="round"
  />
</svg>

we can animate the stroke of the path element by turning it into a motion.path

<svg>
  <motion.path
    d="M5 13l4 4L19 7"
    strokeLinecap="round"
    strokeLinejoin="round"
  />
</svg>

and then using a special property for the animate prop called pathLength:

<svg>
  <motion.path
    animate={{ pathLength: complete ? 1 : 0 }}
    d="M5 13l4 4L19 7"
    strokeLinecap="round"
    strokeLinejoin="round"
  />
</svg>

Unlike opacity and backgroundColor from above, pathLength is not a CSS property but rather a special property provided by Framer Motion to help us with this exact use case. A value of 0 will start off with 0% of the path drawn, and 1 will draw 100% of the path's length.

Mount animations

Now in our case, we don't want to animate the check after a React state change. Instead, we want the path to animate as soon as it's mounted to the DOM.

To animate a property on mount, we can use the initial prop:

<svg>
  <motion.path
    initial={{ pathLength: 0 }}
    animate={{ pathLength: 1 }}
    d="M5 13l4 4L19 7"
    strokeLinecap="round"
    strokeLinejoin="round"
  />
</svg>

As soon as this element is rendered, Framer Motion will start off the pathLength at 0 and then immediately animate it to 1, smoothly drawing out the path to 100% of its length.

Transitions

Every property we animate on a motion element gets a default transition. Transitions determine things like how long an animation takes and what easing curve it uses – so customizing them can have huge impact on the overall look and feel of our interactions.

We can customize a transition using the transition prop on any motion element:

<svg>
  <motion.path
    initial={{ pathLength: 0 }}
    animate={{ pathLength: 1 }}
    transition={{
      delay: 0.2,
      type: "tween",
      ease: "easeOut",
      duration: 0.3,
    }}
    d="M5 13l4 4L19 7"
  />
</svg>

Here, we're using a "tween" animation type, which is a traditional keyframe-based transition, with a short delay and ease-out style easing function. (You may recognize some of the easing functions from CSS transitions.)

Be sure to check out the types and docs for all the different options you can use to customize your transitions.

Buy Framer Motion Recipes

Buy the course

$199one-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
  • New videos added regularly
  • Refactoring videos on React
  • Private Discord
  • Summaries with code

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!