Framer Motion Animation Examples: 12 Copy-Paste UI Animations That Make Vibe-Coded Apps Feel Premium
Vibe coding gets you to a working app fast — but it usually looks like a prototype. These 12 copy-paste Framer Motion animations close the gap between "it works" and "it feels expensive". Drop them into any Next.js admin dashboard today.

💡 Key Takeaways (TL;DR)
- Motion is the single cheapest upgrade you can make to a vibe-coded admin template — no design system rewrite required.
- Use
motion/react,AnimatePresence, and layout animations for 90% of real dashboard interactions.- Always respect
prefers-reduced-motionand animate onlytransform/opacityto keep 60fps on low-end devices.- Twelve ready-to-paste snippets: sidebar, modal, toast, table rows, tabs, counters, skeleton loaders, and more.
Why Vibe-Coded Apps Feel Cheap (And How Motion Fixes It)#
You shipped it. The CRUD works, the auth works, the tables render. And yet when you show it to someone, they say "cool prototype" — not "cool product".
That gap is almost never about your logic. It's about motion.
Static UI reads as a wireframe. A UI that responds — that slides, springs, fades and settles — reads as designed. This is the single highest-ROI upgrade you can make to a vibe coding admin template or any AI-scaffolded Next.js app, and it takes about 40 lines of code per interaction.
Below are 12 production-tested Framer Motion examples you can paste directly into your project. No design system rewrite. No animation degree.
Core Concepts: The 4 Primitives You Actually Need#
Before the snippets, internalize these four. They cover 90% of dashboard interactions.
| Primitive | Import | Use It For |
|---|---|---|
motion.* | motion/react | Any element that animates on mount, hover, or state change |
AnimatePresence | motion/react | Exit animations — modals, toasts, route changes |
layout prop | motion/react | Auto-animating position/size when layout shifts |
useSpring / useMotionValue | motion/react | Mouse-follow, counters, drag, scroll progress |
Install#
npm install motion
motion/reactis the current package (Framer Motion v11+). If your AI-generated code imports fromframer-motion, that still works — but new projects should usemotion/react.
The Golden Rule#
Animate transform and opacity only. Animating width, height, top, or margin triggers layout recalculation and will stutter on mid-range Android devices. Framer Motion's layout prop is the exception — it uses FLIP under the hood and stays cheap.
The 12 Copy-Paste Animations#
1. The Spring Sidebar (Layout-Aware)#
Your admin shell's sidebar should never snap. Use layout so the content area reflows smoothly.
'use client'; import { motion } from 'motion/react'; import { useState } from 'react'; export function AdminShell({ children }: { children: React.ReactNode }) { const [open, setOpen] = useState(true); return ( <div className="flex h-screen bg-slate-950 text-slate-100"> <motion.aside layout initial={false} animate={{ width: open ? 260 : 72 }} transition={{ type: 'spring', stiffness: 320, damping: 32 }} className="relative overflow-hidden border-r border-slate-800" > <nav className="p-4"> <button onClick={() => setOpen(!open)} className="mb-6 rounded-lg bg-slate-800 px-3 py-2 text-sm" > {open ? 'Collapse' : 'Expand'} </button> {['Dashboard', 'Orders', 'Customers', 'Settings'].map((item) => ( <motion.div key={item} whileHover={{ x: 4 }} className="mb-1 cursor-pointer rounded-lg px-3 py-2 text-sm text-slate-400 hover:bg-slate-900 hover:text-white" > {item} </motion.div> ))} </nav> </motion.aside> <main className="flex-1 overflow-y-auto p-8">{children}</main> </div> ); }
Why it works: layout handles the reflow, the spring handles the feel. initial={false} prevents a jarring animation on first paint.
2. The Modal That Doesn't Teleport#
Most vibe-coded modals pop in with display: none → block. That's the #1 tell of an unpolished app.
'use client'; import { AnimatePresence, motion } from 'motion/react'; export function Modal({ open, onClose, children, }: { open: boolean; onClose: () => void; children: React.ReactNode; }) { return ( <AnimatePresence> {open && ( <> <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.18 }} onClick={onClose} className="fixed inset-0 z-40 bg-black/60 backdrop-blur-sm" /> <motion.div initial={{ opacity: 0, scale: 0.95, y: 12 }} animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.97, y: 8 }} transition={{ type: 'spring', stiffness: 400, damping: 30 }} role="dialog" aria-modal="true" className="fixed left-1/2 top-1/2 z-50 w-[min(560px,90vw)] -translate-x-1/2 -translate-y-1/2 rounded-2xl border border-slate-800 bg-slate-900 p-6 shadow-2xl" > {children} </motion.div> </> )} </AnimatePresence> ); }
Key detail: never use -translate-x-1/2 alongside an animated y in the same animate object — Framer Motion writes transform directly and will clobber your Tailwind classes. Here we use y on the inner element only after centering with the class, so keep x/y values small and let CSS handle the 50% offset.
3. Staggered Table Rows#
Dense data tables feel alive when rows cascade in.
'use client'; import { motion } from 'motion/react'; const container = { hidden: { opacity: 0 }, show: { opacity: 1, transition: { staggerChildren: 0.04 } }, }; const row = { hidden: { opacity: 0, y: 8 }, show: { opacity: 1, y: 0 }, }; export function OrdersTable({ orders }: { orders: { id: string; total: number }[] }) { return ( <motion.tbody variants={container} initial="hidden" animate="show"> {orders.map((o) => ( <motion.tr key={o.id} variants={row} whileHover={{ backgroundColor: 'rgba(51,65,85,0.4)' }} className="border-b border-slate-800" > <td className="px-4 py-3">{o.id}</td> <td className="px-4 py-3">${o.total}</td> </motion.tr> ))} </motion.tbody> ); }
Keep staggerChildren between 0.03 and 0.06. Above 0.1 it feels sluggish on 50+ row tables.
4. Toast Stack With Exit Animation#
'use client'; import { AnimatePresence, motion } from 'motion/react'; export function ToastStack({ toasts }: { toasts: { id: string; msg: string }[] }) { return ( <div className="fixed bottom-6 right-6 z-50 flex w-80 flex-col gap-2"> <AnimatePresence mode="popLayout"> {toasts.map((t) => ( <motion.div key={t.id} layout initial={{ opacity: 0, x: 60, scale: 0.9 }} animate={{ opacity: 1, x: 0, scale: 1 }} exit={{ opacity: 0, x: 60, scale: 0.9 }} transition={{ type: 'spring', stiffness: 380, damping: 30 }} className="rounded-xl border border-slate-700 bg-slate-900 px-4 py-3 text-sm shadow-xl" > {t.msg} </motion.div> ))} </AnimatePresence> </div> ); }
mode="popLayout" is the magic — remaining toasts slide up to fill the gap instead of jumping.
5. Animated Counter (Dashboard KPIs)#
Numbers that tick up read as live data.
'use client'; import { animate, useMotionValue, useTransform, motion } from 'motion/react'; import { useEffect } from 'react'; export function Counter({ to, suffix = '' }: { to: number; suffix?: string }) { const count = useMotionValue(0); const rounded = useTransform(count, (v) => Math.round(v).toLocaleString()); useEffect(() => { const controls = animate(count, to, { duration: 1.1, ease: 'easeOut' }); return controls.stop; }, [to, count]); return ( <span className="text-3xl font-semibold tabular-nums"> <motion.span>{rounded}</motion.span> {suffix} </span> ); }
tabular-nums prevents the width from jittering as digits change. Small detail, huge polish.
6. Tab Indicator That Slides (Layout ID)#
'use client'; import { motion } from 'motion/react'; import { useState } from 'react'; export function Tabs({ tabs }: { tabs: string[] }) { const [active, setActive] = useState(tabs[0]); return ( <div className="inline-flex rounded-xl bg-slate-900 p-1"> {tabs.map((tab) => ( <button key={tab} onClick={() => setActive(tab)} className="relative rounded-lg px-4 py-2 text-sm text-slate-400" > {active === tab && ( <motion.span layoutId="tab-pill" className="absolute inset-0 rounded-lg bg-slate-700" transition={{ type: 'spring', stiffness: 420, damping: 34 }} /> )} <span className={`relative z-10 ${active === tab ? 'text-white' : ''}`}>{tab}</span> </button> ))} </div> ); }
layoutId shared between components is the most underrated Framer Motion feature — it animates across mounts.
7. Skeleton → Content Crossfade#
'use client'; import { AnimatePresence, motion } from 'motion/react'; export function AsyncPanel({ loading, children }: { loading: boolean; children: React.ReactNode }) { return ( <div className="relative min-h-[200px]"> <AnimatePresence mode="wait"> {loading ? ( <motion.div key="skeleton" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="space-y-3" > {[0, 1, 2].map((i) => ( <motion.div key={i} animate={{ opacity: [0.4, 0.8, 0.4] }} transition={{ duration: 1.4, repeat: Infinity, delay: i * 0.15 }} className="h-12 rounded-xl bg-slate-800" /> ))} </motion.div> ) : ( <motion.div key="content" initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0 }} > {children} </motion.div> )} </AnimatePresence> </div> ); }
8–12: The Quick Wins#
These are short enough to paste inline.
8. Card hover lift
<motion.div whileHover={{ y: -4, scale: 1.01 }} whileTap={{ scale: 0.99 }} transition={{ type: 'spring', stiffness: 400, damping: 25 }} className="rounded-2xl border border-slate-800 bg-slate-900 p-5" > Revenue </motion.div>
9. Dropdown menu with origin-aware scale
<AnimatePresence> {open && ( <motion.ul initial={{ opacity: 0, scale: 0.95, y: -6 }} animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.97, y: -4 }} transition={{ duration: 0.15, ease: 'easeOut' }} style={{ transformOrigin: 'top left' }} className="absolute mt-2 w-48 rounded-xl border border-slate-800 bg-slate-900 p-1 shadow-xl" > <li className="rounded-lg px-3 py-2 text-sm hover:bg-slate-800">Edit</li> <li className="rounded-lg px-3 py-2 text-sm hover:bg-slate-800">Duplicate</li> </motion.ul> )} </AnimatePresence>
10. Scroll progress bar
'use client'; import { motion, useScroll, useSpring } from 'motion/react'; export function ScrollProgress() { const { scrollYProgress } = useScroll(); const scaleX = useSpring(scrollYProgress, { stiffness: 140, damping: 30 }); return ( <motion.div style={{ scaleX }} className="fixed left-0 top-0 z-50 h-0.5 w-full origin-left bg-cyan-400" /> ); }
11. Route transition wrapper (Next.js App Router)
'use client'; import { motion } from 'motion/react'; export function PageTransition({ children }: { children: React.ReactNode }) { return ( <motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.25, ease: [0.22, 1, 0.36, 1] }} > {children} </motion.div> ); }
Wrap it in each page.tsx. For exit animations you need AnimatePresence keyed on usePathname() in a layout.
12. Reduced-motion global wrapper
'use client'; import { MotionConfig } from 'motion/react'; export function MotionProvider({ children }: { children: React.ReactNode }) { return <MotionConfig reducedMotion="user">{children}</MotionConfig>; }
Wrap your root layout in this once. Framer Motion then automatically disables transform animations for users with prefers-reduced-motion — accessibility handled in one line.
Best Practices & Performance Guardrails#
| Rule | Why |
|---|---|
Animate transform/opacity only | Avoids layout thrash; keeps 60fps |
| Durations: 150–300ms for UI, 400–600ms for page-level | Anything slower feels broken |
Use spring for physical elements, easeOut for opacity | Springs feel natural; opacity should be linear-ish |
| Never animate more than ~3 properties per element | Diminishing returns, rising cost |
Add 'use client' at the top of every animated file | Framer Motion requires the client runtime |
Always ship MotionConfig reducedMotion="user" | WCAG 2.3.3 compliance |
Avoid AnimatePresence inside virtualized lists | Exit animations conflict with row recycling |
The Most Common Mistake#
AI-generated code loves this pattern:
// ❌ Breaks your Tailwind translate classes <motion.div animate={{ y: 0 }} className="-translate-y-1/2" />
Framer Motion writes the full transform string, wiping out Tailwind's translate utilities. Fix it by animating a wrapper and positioning the child with CSS, or by using style={{ y }} with a motion value instead of the animate prop.
Debugging Tip#
Set transition={{ duration: 3 }} temporarily. Watching an animation in slow motion reveals exactly which property is fighting your layout. Then set it back.
FAQ: Framer Motion in Vibe-Coded Next.js Apps#
Do these work with React Server Components?
No. Framer Motion requires the client runtime. Add 'use client' to any file that imports motion/react. Your page.tsx can stay a server component if it only renders client children.
Will 12 animations tank my bundle?
motion/react adds roughly 30–35kB gzipped. If you only need two or three effects, use the tree-shakeable motion/react-m entry with LazyMotion and the domAnimation feature bundle to cut it to ~15kB.
Should I animate my whole app or just the shell? Start with the shell — sidebar, page transitions, toasts. Those are seen on every single navigation. Then add table staggers and KPI counters. Leave decorative animations out; restraint reads as premium.
Your Next Step#
Motion is the cheapest, fastest way to make a vibe-coded app stop looking like a prototype. Pick three animations from this list — sidebar, modal, and toasts — and ship them today. That's a 30-minute change that shifts how users perceive the entire product.
Want a head start? Grab a production-ready Next.js dashboard with these motion primitives already wired in, then swap in your own logic.
Frequently Asked Questions#
Do these Framer Motion examples work with React Server Components in the Next.js App Router?#
Not directly. Framer Motion needs the client runtime, so every file that imports motion/react must start with 'use client'. Your page.tsx can remain a server component as long as the animated pieces are separate client components you render inside it.
How much bundle size does Framer Motion add to a vibe coding admin template?#
The full motion/react import is roughly 30–35kB gzipped. If you only need a handful of effects, use motion/react-m with LazyMotion and the domAnimation feature bundle to bring it down to around 15kB.

