How to Add Animations to Shadcn UI Components with Framer Motion
Learn how to animate Shadcn UI components with Framer Motion. Covers entrance animations, hover effects, page transitions, scroll-triggered reveals, AnimatePresence, and accessibility with prefers-reduced-motion.
SERP Blocks Team
Product
Static interfaces get the job done. Animated interfaces get remembered. The difference between a page that loads and a page that feels alive often comes down to a few well-placed transitions — a hero section that fades into view, feature cards that stagger in as you scroll, a modal that slides up instead of popping into existence.
Framer Motion has become the animation library of choice in the React ecosystem, and it pairs exceptionally well with Shadcn UI components. Because Shadcn UI gives you the actual component source code rather than a locked-down package, you can wrap, extend, and animate any component without fighting abstractions or overriding internal styles.
This guide covers practical animation patterns you can apply immediately: entrance animations for hero and feature sections, hover micro-interactions, page transitions with AnimatePresence, scroll-triggered reveals, and the accessibility considerations that ensure your animations enhance rather than harm the user experience.
Why Framer Motion Works Well with Shadcn UI
Framer Motion operates by wrapping HTML elements with its motion components — motion.div, motion.span, motion.button, and so on. These are drop-in replacements for their HTML counterparts that accept animation props like initial, animate, exit, whileHover, and whileInView.
Shadcn UI components are built with Radix UI primitives and styled with Tailwind CSS. They render standard HTML elements. This means you can wrap any Shadcn component in a motion.div without breaking its functionality, or use motion directly on elements inside Shadcn component source files.
There is no conflict between Tailwind's utility classes and Framer Motion's animation system. Tailwind handles the static styles (colors, spacing, typography, layout). Framer Motion handles the dynamic transforms and opacity changes. They operate on different layers and complement each other cleanly.
Installing Framer Motion
Add Framer Motion to your Next.js project:
npm install framer-motionBecause Framer Motion relies on React context and event handlers, animated components need to run on the client. Add the "use client" directive at the top of any component file that uses motion:
"use client";
import { motion } from "framer-motion";If you are building a page that is mostly server-rendered, extract the animated portions into separate client components and compose them into your server component page. This keeps the bundle lean and lets you benefit from server rendering for everything that does not need animation.
Entrance Animations with motion.div
The most impactful animations are often the simplest. A hero section that fades in and shifts up by 20 pixels feels polished. The same hero without any transition feels abrupt.
Here is a basic entrance animation applied to a hero section:
"use client";
import { motion } from "framer-motion";
import { Button } from "@/components/ui/button";
export function AnimatedHero() {
return (
<section className="flex min-h-[80vh] items-center justify-center px-6">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="max-w-3xl text-center"
>
<h1 className="text-4xl font-bold tracking-tight sm:text-6xl">
Build Faster with Pre-Built Blocks
</h1>
<p className="mt-6 text-lg text-muted-foreground">
Over 1,200 production-ready UI sections for React, Next.js,
and Tailwind CSS.
</p>
<div className="mt-10 flex items-center justify-center gap-4">
<Button size="lg">Browse Blocks</Button>
<Button size="lg" variant="outline">
View Pricing
</Button>
</div>
</motion.div>
</section>
);
}The initial prop sets the starting state (invisible and shifted down). The animate prop defines the end state (fully visible, at its natural position). The transition prop controls timing and easing.
SERP Blocks includes 81 hero sections with various layouts — centered, split, with background images, with video, with gradient overlays. Each of these can be enhanced with entrance animations using this same pattern. Wrap the content container in a motion.div and define the initial-to-animate transition.
Staggered Animations for Feature Grids
When you have multiple items appearing together — feature cards, team members, pricing tiers — staggering their entrance creates a cascading reveal that guides the eye across the content.
Framer Motion handles this with staggerChildren on a parent variant:
"use client";
import { motion } from "framer-motion";
import { Card, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.2,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5, ease: "easeOut" },
},
};
const features = [
{ title: "81 Hero Sections", description: "Full-width, split, centered, and video hero layouts." },
{ title: "132 Feature Blocks", description: "Grid, bento, icon-based, and comparison feature sections." },
{ title: "110 Card Components", description: "Content, pricing, profile, and interactive card designs." },
{ title: "40 Testimonials", description: "Carousel, grid, single-quote, and social-proof layouts." },
];
export function AnimatedFeatures() {
return (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4"
>
{features.map((feature) => (
<motion.div key={feature.title} variants={itemVariants}>
<Card>
<CardHeader>
<CardTitle>{feature.title}</CardTitle>
<CardDescription>{feature.description}</CardDescription>
</CardHeader>
</Card>
</motion.div>
))}
</motion.div>
);
}The parent motion.div uses containerVariants, which staggers its children by 100 milliseconds each. Each child motion.div uses itemVariants — it inherits the hidden and visible state names from its parent automatically. The first card appears, then 100ms later the second, then the third, and so on.
This pattern works for any collection. The 132 feature section blocks in SERP Blocks use grid layouts that are perfect candidates for staggered entrance animations. The same approach applies to the 23 team sections, 21 pricing pages, and 25 gallery layouts.
Hover Micro-Interactions
Hover effects provide immediate feedback that makes interfaces feel responsive. Framer Motion's whileHover prop handles this without CSS pseudo-classes, giving you access to spring physics and complex transforms.
A subtle scale and shadow lift on cards:
<motion.div
whileHover={{ scale: 1.02, y: -4 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
>
<Card className="transition-shadow hover:shadow-lg">
<CardHeader>
<CardTitle>Pro Plan</CardTitle>
<CardDescription>One-time purchase, lifetime access</CardDescription>
</CardHeader>
</Card>
</motion.div>The spring physics create a natural, physical feel — the card does not move linearly but instead overshoots slightly and settles. Adjust stiffness (higher = snappier) and damping (higher = less bounce) to match your brand's personality.
For buttons, a tap effect adds satisfying feedback:
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.97 }}
className="rounded-md bg-primary px-6 py-3 text-primary-foreground"
>
Get Started
</motion.button>The whileTap scales the button down slightly on press, mimicking the feel of a physical button being pushed. Combined with the hover scale-up, this creates a complete interaction arc.
Scroll-Triggered Animations with whileInView
Entrance animations that fire on page load only work for content above the fold. For everything below — feature grids, testimonial sections, CTA blocks, footer content — you need scroll-triggered animations.
Framer Motion's whileInView prop handles this with an IntersectionObserver under the hood:
<motion.section
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.7, ease: "easeOut" }}
>
<h2 className="text-3xl font-bold">What Our Users Say</h2>
{/* Testimonial content */}
</motion.section>The viewport prop configures the IntersectionObserver. Setting once: true means the animation fires once and never reverses — the content stays visible after the user scrolls past. The margin: "-100px" triggers the animation when the element is 100 pixels inside the viewport, which feels more natural than triggering at the exact viewport edge.
Combine whileInView with staggered children for scroll-triggered cascading reveals:
const sectionVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.15 },
},
};
const cardVariants = {
hidden: { opacity: 0, y: 30 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5 },
},
};
export function ScrollRevealGrid() {
return (
<motion.div
variants={sectionVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-80px" }}
className="grid gap-6 md:grid-cols-3"
>
{items.map((item) => (
<motion.div key={item.id} variants={cardVariants}>
<Card>{/* card content */}</Card>
</motion.div>
))}
</motion.div>
);
}This pattern is particularly effective for long pages with multiple sections. A landing page built with SERP Blocks might combine a hero section, a feature grid, a testimonial carousel, a pricing table, a FAQ accordion, and a CTA section. Each section can scroll-reveal independently, creating a page that unfolds progressively as the visitor reads.
Page Transitions with AnimatePresence
AnimatePresence is Framer Motion's solution for animating components as they mount and unmount. In Next.js, this is most commonly used for page transitions and for modal/dialog enter-exit animations.
For route-based page transitions in the App Router, create a template component:
"use client";
import { motion } from "framer-motion";
export default function Template({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: "easeOut" }}
>
{children}
</motion.div>
);
}Place this template.tsx file in your route group (for example, inside app/(marketing)/template.tsx), and every page navigation within that group will fade in. The App Router remounts the template component on navigation, so the initial animation fires on each route change.
For animating elements that conditionally appear and disappear — like notification toasts, modals, or expanding sections — wrap them with AnimatePresence:
"use client";
import { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
export function ExpandableCard() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<Button onClick={() => setIsOpen(!isOpen)}>
{isOpen ? "Collapse" : "Expand"}
</Button>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
>
<Card className="mt-4 p-6">
<p>This content slides in and out smoothly.</p>
</Card>
</motion.div>
)}
</AnimatePresence>
</div>
);
}The exit prop defines the animation that plays when the component unmounts. Without AnimatePresence, React immediately removes the element from the DOM. AnimatePresence delays the removal until the exit animation completes.
Layout Animations
Framer Motion's layout prop automatically animates elements when their layout changes — when items are filtered, reordered, or resized. This is powerful for interactive components like filterable grids and sortable lists.
<motion.div layout className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{filteredItems.map((item) => (
<motion.div
key={item.id}
layout
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
>
<Card>{/* item content */}</Card>
</motion.div>
))}
</motion.div>When filteredItems changes — say a user applies a category filter on a blog list page or a product list grid — the remaining cards smoothly slide into their new positions instead of jumping. This is one line of code (layout prop) that makes a dramatic difference in perceived quality.
Respecting Reduced Motion Preferences
Accessibility is not optional. Some users have vestibular disorders where motion causes dizziness, nausea, or disorientation. Operating systems provide a "reduce motion" preference, and your animations must respect it.
Create a hook that reads the preference and provides safe animation values:
"use client";
import { useReducedMotion } from "framer-motion";
export function useAnimationConfig() {
const shouldReduceMotion = useReducedMotion();
return {
fadeUp: shouldReduceMotion
? { initial: { opacity: 0 }, animate: { opacity: 1 } }
: { initial: { opacity: 0, y: 30 }, animate: { opacity: 1, y: 0 } },
stagger: shouldReduceMotion ? 0 : 0.1,
duration: shouldReduceMotion ? 0.1 : 0.5,
};
}When reduced motion is enabled, the hook strips out transforms (the y movement) and shortens durations. Opacity transitions are generally safe because they do not involve spatial movement. Use this hook throughout your animated components:
export function AnimatedSection({ children }: { children: React.ReactNode }) {
const { fadeUp, duration } = useAnimationConfig();
return (
<motion.div
initial={fadeUp.initial}
whileInView={fadeUp.animate}
viewport={{ once: true }}
transition={{ duration }}
>
{children}
</motion.div>
);
}Framer Motion also provides its own useReducedMotion hook that returns a boolean, which is what we used above. This is more reliable than a CSS media query for JavaScript-driven animations because it ensures the motion values themselves change, not just the CSS.
Performance Considerations
Animation performance comes down to one rule: animate transform and opacity, avoid animating everything else. Framer Motion handles this well by default — x, y, scale, rotate, and opacity are all GPU-accelerated properties that do not trigger layout recalculations.
Avoid animating width, height, padding, or margin directly. If you need to animate size changes, use scale transforms or Framer Motion's layout prop, which uses FLIP (First, Last, Invert, Play) animations to achieve layout transitions using only transforms.
For pages with many animated elements, be strategic. A landing page with a hero section, feature blocks, testimonials, and CTA sections does not need every element animating simultaneously. Use scroll-triggered animations so only the visible section animates, and set viewport={{ once: true }} so completed animations do not re-fire when scrolling back up.
Keep animation durations between 200ms and 700ms. Anything shorter feels jarring. Anything longer feels sluggish. For entrance animations, 400-600ms is the sweet spot. For hover effects, 150-250ms keeps interactions feeling snappy.
Putting It All Together
A complete landing page animation strategy might look like this:
Hero section: Immediate fade-up on page load (no scroll trigger needed since it is above the fold). Duration: 600ms.
Feature grid: Scroll-triggered staggered reveal. Each card fades up with 100ms stagger. The grid animates when it enters the viewport.
Testimonials: Scroll-triggered fade-in. Individual testimonial cards in the 40 testimonial layouts available in SERP Blocks can stagger for added effect.
Pricing table: Scroll-triggered. The popular/recommended plan can animate with a slight delay to draw attention after the other plans appear.
FAQ section: The 21 FAQ accordion designs already have open/close transitions via Radix UI. Framer Motion can add a scroll-triggered entrance for the section itself.
CTA section: Scroll-triggered fade-up. Keep it simple — the content should speak for itself.
Reduced motion: All of the above respect the user's motion preference, falling back to simple opacity fades.
With SERP Blocks providing over 1,200 pre-built sections across 50+ categories — from hero layouts to dashboard interfaces to e-commerce flows — and 60 free blocks to start with, the foundation is already built. Adding Framer Motion animations on top transforms those sections from functional to memorable, and as this guide has shown, the implementation is straightforward once you understand the core patterns.