framer-motion takes the tedious manual work out of animation. Instead of managing keyframes and timing yourself, you only describe the target state and the library computes the path there. This guide takes you from your first motion to staggered lists and shows what matters for calm, high-quality motion.
- framer-motion does not replace CSS transitions. It pays off where states come and go.
- Three properties are enough to start: initial, animate and transition.
- Stagger instead of all at once: a few tenths of a second turn one motion into an order.
- prefers-reduced-motion belongs in every component. It is one line of code and for some people the difference.
The motion element as the base
Every animation starts with a motion element. A div becomes motion.div, a button becomes motion.button. These elements behave like their normal counterparts but understand extra props for motion.
With the animate prop you define how the element should look. A value like animate with opacity 1 and y 0 describes the target state. When this value changes, framer-motion animates there smoothly without you calculating the path in between.
For the start state you use initial. This way an element fades in from opacity 0 and a slight shift on load. That produces the typical calm slide-up that shapes the first impression on many pages.
- initial
- opacity 0, y 12
- animate
- opacity 1, y 0
- duration
- 0.5 s
- ease
- cubic-bezier(.22, 1, .36, 1)
- stagger
- 80 ms
Twelve pixels of travel and half a second. A hero needs no more.
The same path, just started offset. The switch shows what happens with reduced motion.
<motion.div initial={{ opacity: 0, y: 22 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }} Weitfeld</motion.div>
Control transitions with transition
The transition prop determines the pace and character of the motion. With duration you set the time in seconds, with ease the acceleration curve. Values between 0.3 and 0.6 seconds feel pleasant for most UI motions.
Instead of a curve you can also pick a spring. With type spring the motion responds to stiffness and damping and feels more physical. For calm interfaces a soft curve like [0.22, 1, 0.36, 1] often stays the better choice, because it eases out more controlled.
The same curve, three durations. The usable range is narrower than it feels while building.
Three curves cover everyday work.
A page with three curves feels ordered, one with twelve feels random. These three are almost always enough.
Variants for clean states
As soon as an element has several states, inline values get messy. Variants solve this: you define named states like hidden and visible as an object and reference them through initial and animate by their name only.
The big advantage shows with nested elements. A parent can pass its variants down to all children. That way you control a whole group with a single state change, without addressing each child individually.
const auftauchen = { hidden: { opacity: 0, y: 18 }, show: { opacity: 1, y: 0 },}; <motion.li variants={auftauchen} />
Stagger lists with staggerChildren
When many elements appear at once, it feels flat. With staggerChildren in the parent variant you set a small offset between the children, usually 0.05 to 0.12 seconds. The elements then appear one after another and the eye is guided.
For this to work, each child gets the same variant names as the parent. framer-motion handles the order and the spacing. For cards, navigation items or feature lists this is an effective, subtle effect.
const liste = { hidden: {}, show: { transition: { staggerChildren: 0.08 } },};
Animate on scroll
Many motions should only trigger once an element enters the visible area. For that you replace animate with whileInView and use viewport to define when and how often the animation starts. With once true it runs only the first time it becomes visible.
This avoids content further down having played its fade-in before the user even sees it. The result is a page that builds up calmly and lively as you scroll.
Keep motion accessible
Not everyone wants to see motion. The hook useReducedMotion from framer-motion reads the system setting prefers-reduced-motion. When it is active you deliver a calmer variant, for example just a fade-in without any shift.
Also keep animations short and avoid strong scaling, rotations or hectic jumping. Good motion supports the content and never pushes itself into the foreground.
const reduce = useReducedMotion(); <motion.div initial={reduce ? false : { opacity: 0, y: 22 }} animate={{ opacity: 1, y: 0 }}/>
The three most common beginner mistakes
Almost every problem with framer-motion is not a bug in the library but an expectation that does not match how it works.
- whileInView on absolutely positioned children. The observer does not report such elements reliably, and then the content stays invisible forever. Attach it to a box with real height.
- Animating width or height to auto. The target value gets measured mid-motion, and that visibly stutters. Measure once yourself and animate in pixels.
- Starting everything at once. Ten elements with the same delay feel like a jump. An offset of 0.06 to 0.12 seconds turns it into an order.
- Animating height from 0 to auto.
- Using top and left for a shift.
- Ten elements with the same delay.
- Scaling from 0.6 to 1 on large surfaces.
- Animate a measured pixel height.
- Use transform via x and y.
- 0.06 to 0.12 seconds of offset per element.
- From 0.98 to 1, barely visible and still felt.
If a motion needs explaining, it was too big.
Taking reduced motion seriously
Some people get dizzy or a headache from moving interfaces. Their operating system tells the page, if you ask.
In framer-motion you ask with useReducedMotion. The rest is a decision per component: show the final state right away instead of just speeding the motion up. A faster animation is still an animation.







