Building stunning animated UI with Motion and Tailwind CSS in Next.js
A practical guide to building fluid, interactive interfaces with Motion and Tailwind CSS in a Next.js App Router project, without hurting performance.
On this page
Animation is not decoration. Used well, it explains where something came from, confirms an action, and makes an interface feel responsive. Used badly, it makes a product feel slow.
This guide covers the patterns I actually use: small, purposeful motion that is cheap to run and easy to remove.
Start with the user's preference
Every animation should be optional. Respecting prefers-reduced-motion is not a nice-to-have, it is a baseline requirement.
"use client";
import { MotionConfig } from "motion/react";
export function Providers({ children }: { children: React.ReactNode }) {
return <MotionConfig reducedMotion="user">{children}</MotionConfig>;
}
Wrapping the app once means every component inherits the behaviour. You do not have to remember it in each file.
Animate transforms, not layout
The browser can composite transform and opacity on the GPU. Animating width, height, top, or left forces layout on every frame.
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: "easeOut" }}
/>
y is a shorthand for translateY, so this stays on the compositor.
Stagger lists for rhythm
A little offset between items reads as intentional rather than abrupt.
const container = {
hidden: {},
show: { transition: { staggerChildren: 0.06 } },
};
const item = {
hidden: { opacity: 0, y: 16 },
show: { opacity: 1, y: 0 },
};
<motion.ul variants={container} initial="hidden" animate="show">
{items.map((value) => (
<motion.li key={value} variants={item} />
))}
</motion.ul>
Keep the total sequence under roughly 400ms. Past that, the user is waiting for your animation instead of doing their task.
Hover and press feedback
Micro-interactions should be instant. Hover is about affordance, press is about confirmation.
- Hover: a subtle background change or a small lift
- Press: a slight scale down, around
0.98 - Focus: a visible ring, never removed
<button className="transition-colors hover:bg-black/5 active:scale-[0.98] focus-visible:ring-2">
Copy
</button>
If you reach for Motion for a colour change, a CSS transition is simpler and cheaper.
When to reach for JavaScript motion
Use Motion when the animation depends on state that CSS cannot express:
| Scenario | Tool |
|---|---|
| Colour, shadow, small translate | CSS transition |
| Enter and exit of dynamic content | AnimatePresence |
| Shared element between routes | layoutId |
| Gesture-driven or spring physics | Motion |
Watch the cost
Motion is a real dependency. Keep it out of the critical path where you can and remember that every animated element is also a paint the browser has to perform.
Keep animations under 300ms for feedback and under 400ms for entrances. If it feels slow, it is slow.
The best animation is the one the user never consciously notices.
Continue reading
Related articles
Serverless file uploads with Supabase Storage and Next.js API routes
Secure, serverless file uploads in Next.js using Supabase Storage, with validation, size limits, and public access handling.
Building a modern todo app with Zustand, Firebase, and Next.js
A real-time todo app with authentication, CRUD operations, and predictable global state, built with Next.js, Firebase, and Zustand.
Integrating Razorpay payments in Next.js: a step-by-step guide
Set up checkout, verify signatures on the server, and handle webhooks safely when integrating Razorpay with Next.js.