Published September 20, 2026
Motion (ex Framer Motion): Animations, UX, and Performance
What UI animations are, why they matter for UX, and how to keep Motion (ex Framer Motion) fast: GPU-friendly properties, LazyMotion, and reduced motion.
Animations are one of the fastest ways to make an interface feel alive, and one of the fastest ways to make it feel slow, noisy, or broken. The difference is rarely the library. It is what you animate, why, and how.
In this post we will look at:
- what a UI animation really is, and why we use them,
- how they affect UX (for better and for worse),
- how to keep them fast with Motion, the library formerly known as Framer Motion,
- a checklist of best choices you can apply today.
TL;DR. Animate to explain a change, not to decorate. Stick to transform
and opacity, keep most transitions short (roughly 150 to 300 ms), ship the
smallest Motion bundle you can, and always respect prefers-reduced-motion.
Framer Motion is now Motion
Framer Motion started as the animation library of Framer, the website builder. It is now an independent project simply called Motion. The API you already know is the same, but the package and the imports changed:
npm install motion // Before
import { motion } from "framer-motion";
// Now
import { motion } from "motion/react";If you use the Next.js App Router, there is also motion/react-client. It lets you use motion components directly from a Server Component file without adding "use client" yourself:
import * as motion from "motion/react-client";
export default function Page() {
return (
<motion.h1 initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }}>
Hello, Motion
</motion.h1>
);
}What is a UI animation?
An animation is simply a value that changes over time: an opacity going from 0 to 1, a card moving 12 px up, a panel scaling from 0.95 to 1. The browser draws many frames in between, and your brain reads them as movement.
In a product, animations usually fall into a few families:
| Family | Example | Purpose |
|---|---|---|
| Micro-interaction | Button press, hover, toggle | Confirm that the UI heard you |
| Enter and exit | Modal, toast, dropdown | Show where something comes from and goes to |
| Layout change | Reordering a list, expanding a card | Keep the user oriented while things move |
| Scroll-linked | Reading progress bar, parallax | Connect the page to the user's position |
| Loading and feedback | Skeleton, spinner, success check | Make waiting feel shorter and clearer |
| Decorative | Floating shapes, background loops | Personality (use sparingly) |
Why do we animate at all?
Because the screen has no physical continuity. In the real world, a drawer slides out. On a screen, without animation, it simply appears. Motion gives the eye a path to follow.
Good animations do real work:
- Feedback. A pressed button that scales down slightly says "I got your click".
- Orientation. A page or panel that slides in from the right tells you where you are in the flow.
- Continuity. A card that grows into a detail view keeps the user's mental model intact.
- Attention. A subtle change on a new notification points to what changed.
- Perceived performance. A skeleton or a short transition makes a wait feel intentional instead of frozen.
Animation is not free. Every animated element costs attention from the user and CPU or GPU time from the device. If an animation does not explain, confirm, or guide, remove it.
The impact on UX
When it helps
People perceive a response under about 100 ms as instant, which is why hover and press feedback should feel immediate. For transitions, a common rule of thumb is:
- Small feedback (hover, tap, toggle): roughly 100 to 200 ms.
- Most UI transitions (menus, dialogs, tabs): roughly 150 to 300 ms.
- Large movements (full page or big panel): up to about 500 ms.
These are guidelines, not specifications. Test them with real people and real devices.
Easing matters as much as duration. Things entering the screen usually look best when they decelerate (fast start, soft landing). Springs are a great default in Motion because they feel physical and handle interruptions gracefully:
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ type: "spring", stiffness: 260, damping: 24 }}
/>When it hurts
- Slow animations on frequent actions. A 600 ms menu is charming once and infuriating the fortieth time.
- Animations that block the user. Never make people wait for an animation to finish before they can act.
- Too much movement. Large parallax, spinning, or zooming can cause dizziness or nausea for people with vestibular disorders.
- Motion without meaning. If everything moves, nothing stands out.
Before adding one, ask this:
Performance: what makes an animation fast or slow
The browser turns your code into pixels in roughly three steps: layout (where things go), paint (what they look like), and composite (combining layers on the GPU). The earlier a step you trigger, the more work every frame costs.
At 60 fps you have about 16.7 ms per frame, and only about 8.3 ms at 120 Hz. Miss it, and users see jank.
Choose the right properties
| Property | What it triggers | Verdict |
|---|---|---|
transform (x, y, scale, rotate) | Composite only | Best choice |
opacity | Composite only | Best choice |
filter, clipPath | Usually composite | Good, test on low-end devices |
backgroundColor | Paint | Fine for small elements |
width, height, top, left, margin | Layout, then paint | Avoid on elements that push other content around |
A concrete example. Both bars below "fill up", but only one is cheap:
// Triggers layout on every frame
<motion.div animate={{ width: "100%" }} className="h-1 bg-primary" />
// Composite only
<motion.div
animate={{ scaleX: 1 }}
initial={{ scaleX: 0 }}
style={{ transformOrigin: "0% 50%" }}
className="h-1 w-full bg-primary"
/>Need to animate a size or position change of a real layout element, like reordering a list? Use the layout prop. Motion measures the before and after, then animates with transforms instead of animating layout properties directly:
<motion.li
layout
transition={{ type: "spring", stiffness: 300, damping: 30 }}
/>According to Motion's own performance guide, transform and opacity are the
cheapest values to render in every browser. Beyond those, test across browsers
and low-powered devices before you commit.
Hardware acceleration and the Motion engine
Motion is a hybrid engine. It is built on the Web Animations API (WAAPI), so for values like transform, opacity, filter, and clipPath it can run the animation on the GPU, separately from your JavaScript. That means the animation stays smooth even when the main thread is busy. For everything else, Motion falls back to its JavaScript engine.
One nuance worth knowing: shorthand props like x, y, and scale are independent transforms, which perform great but run on the JavaScript side. Motion documents that setting transform directly is what unlocks hardware acceleration:
<motion.div animate={{ transform: "translateX(100px)" }} />Use it when the main thread is under pressure. In most everyday UI, independent transforms are perfectly fine.
Keep the bundle small
Motion's motion component is around 34 kb and bundlers cannot tree-shake it further. The docs show two ways to go much smaller:
LazyMotion+mbrings the initial cost down to about 4.6 kb, then loads features on demand:domAnimation(+15 kb) covers animations, variants, exit animations and tap, hover, focus gestures;domMax(+25 kb) adds drag and layout animations.useAnimatemini is only 2.3 kb and uses WAAPI exclusively.
"use client";
import { LazyMotion, domAnimation } from "motion/react";
export function MotionProvider({ children }: { children: React.ReactNode }) {
return (
<LazyMotion features={domAnimation} strict>
{children}
</LazyMotion>
);
}"use client";
import * as m from "motion/react-m";
export function FadeIn({ children }: { children: React.ReactNode }) {
return (
<m.div initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }}>
{children}
</m.div>
);
}The strict prop makes Motion throw if someone accidentally uses motion.div inside LazyMotion, which would silently bring the full bundle back.
Need domMax only on one route? Load it asynchronously:
const loadFeatures = () => import("./features").then((res) => res.default);
<LazyMotion features={loadFeatures}>{children}</LazyMotion>;with features.ts containing:
import { domMax } from "motion/react";
export default domMax;Avoid re-renders with motion values
If something updates on every scroll or pointer event, do not put it in React state. Use motion values, which update the DOM without re-rendering your component:
"use client";
import { motion, useScroll, useSpring } from "motion/react";
export function ReadingProgress() {
const { scrollYProgress } = useScroll();
const scaleX = useSpring(scrollYProgress, {
stiffness: 120,
damping: 24,
restDelta: 0.001,
});
return (
<motion.div
style={{ scaleX, transformOrigin: "0% 50%" }}
className="fixed inset-x-0 top-0 z-50 h-1 bg-primary"
/>
);
}This animates scaleX (a transform) driven by scroll, with zero React re-renders.
Do not animate your LCP element
A very common mistake is fading in the hero title or image with initial={{ opacity: 0 }}. That element is invisible until JavaScript loads and hydrates, which can delay your Largest Contentful Paint.
- Do not hide above-the-fold content behind an entrance animation.
- If you want an intro, animate secondary elements, or use
initial={false}so the first render matches the final state.
Transform-based animations do not cause layout shifts, so they are safe for CLS. Animating height or margin can push content around and hurt it.
Scroll-triggered animations
whileInView is convenient, but every observed element has a cost. Animate once and stop observing:
<motion.section
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
/>Accessibility: respect reduced motion
Animations can cause real discomfort. Operating systems expose a Reduced Motion setting, exposed to the web as prefers-reduced-motion. Motion makes it simple to respect it.
Wrap your app once:
import { MotionConfig } from "motion/react";
<MotionConfig reducedMotion="user">{children}</MotionConfig>;With reducedMotion="user", Motion disables transform and layout animations for users who asked for less motion, while keeping things like opacity and backgroundColor. Content still fades in, but nothing slides or zooms.
For finer control, use the hook:
const shouldReduceMotion = useReducedMotion();
<motion.aside
animate={{
opacity: isOpen ? 1 : 0,
x: isOpen ? 0 : shouldReduceMotion ? 0 : "-100%",
}}
/>;Also remember:
- Anything that moves automatically for more than 5 seconds should have a way to pause, stop, or hide it (WCAG 2.2.2).
- Avoid flashing content, and be careful with parallax and auto-playing video.
- Never rely on animation alone to convey information.
A practical workflow
Decide the purpose Write one sentence: "This animation helps the user to
...". If you cannot finish it, skip the animation.
Pick cheap properties Use transform and opacity. For layout changes,
reach for the layout prop.
Keep it short and interruptible Use short durations or springs, and
never block the user's next action.
Ship less JavaScript Use LazyMotion with m, load domMax only where
you need drag or layout animations, and consider useAnimate mini for
simple cases.
Respect reduced motion Add MotionConfig with reducedMotion="user"
once at the root.
Measure on real devices Open Chrome DevTools, record a Performance
profile with CPU throttling, and turn on paint flashing and layout shift regions. Test on a real mid-range phone, not just your laptop.
Common questions
Conclusion
Great motion is mostly restraint: a few purposeful animations, on cheap properties, in a small bundle, that respect the people using your product. Motion gives you a hybrid engine, springs, layout animations, and tools like LazyMotion and MotionConfig to do exactly that.
Want to go deeper? The official docs are the best next step: