Multistep wizard
Learn the basics of state-based animation with Framer Motion.
8 lessons · 3:08:10
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
Get everything in Framer Motion Recipes.
- 3+ hours of video
- 8 lessons
- Private Discord
- Summaries with code
- Unlimited access to course materials
Lifetime membership
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!