Building Animated Shadcn Tabs with Framer Motion
Elevating UI: Beyond Static Tabs Most web applications heavily rely on tab components for organizing content, from dashboards to settings panels. However, many implementations are static and lack engaging interactions.

Elevating UI: Beyond Static Tabs
Most web applications heavily rely on tab components for organizing content, from dashboards to settings panels. However, many implementations are static and lack engaging interactions. What if your tabs could feel alive, featuring smooth spring animations, a dynamic stacked card effect on hover, and an active indicator that gracefully glides between selections? This guide explores building such a component, integrating Shadcn/ui, React, Tailwind CSS, and Framer Motion.
This animated tab system transforms a standard tab switcher into a polished, reusable component with a clear active state and delightful micro-interactions. Key features include a spring-animated active pill indicator, a stacked card effect that fans out on hover, a smooth entrance animation for new active content, and fully theme-aware styling leveraging Shadcn/ui CSS variables.
Prerequisites
Before diving in, ensure you have a solid grasp of React and TypeScript basics, Tailwind CSS utility classes, and familiarity with Shadcn/ui component installation and theming. Your project should also have Shadcn/ui and Framer Motion (or motion/react) already set up, ideally within a Next.js or Vite environment.
Streamlined Installation with Shadcn Space CLI
To kickstart development, we'll leverage Shadcn Space, a registry of production-ready, Shadcn/ui-compatible components. This allows you to pull the animated tab component directly into your project, pre-wired to your existing Shadcn/ui theme tokens, bypassing manual scaffolding.
Consult the Shadcn Space Getting Started guide for detailed instructions on integrating the Shadcn CLI with third-party registries. Once configured, execute one of the following commands based on your package manager:
pnpm typescript pnpm dlx shadcn@latest add @shadcn-space/tabs-01
npm typescript npx shadcn@latest add @shadcn-space/tabs-01
Yarn typescript yarn dlx shadcn@latest add @shadcn-space/tabs-01
Bun typescript bunx --bun shadcn@latest add @shadcn-space/tabs-01
This command scaffolds the component files, ready for immediate use and customization.
Understanding the Component Architecture
The component’s architecture is structured to orchestrate sophisticated animations and content rendering:
typescript AnimatedTabMotion (page/demo entry point) └── Tabs (tab bar + content orchestrator) ├── Tab buttons (with spring-animated active pill) └── FadeInStack (stacked, animated content panels)
Defining Tab Data Types
The component's flexibility begins with its data types. The Tab type specifies the structure of each individual tab item, while TabsProps enables extensive customization through className overrides for various visual elements.
typescript type Tab = { title: string; value: string; content?: React.ReactNode; };
type TabsProps = { tabs: Tab[]; containerClassName?: string; activeTabClassName?: string; tabClassName?: string; contentClassName?: string; };
This design allows independent restyling of the active pill, individual tab buttons, and the content area without modifying core logic.
Building the Tab Data Array
Tab data is provided as an array of Tab objects. Each content property accepts any React.ReactNode, meaning you can pass arbitrary JSX as the panel body. The example utilizes Shadcn/ui semantic tokens (bg-muted, text-foreground, border-border) ensuring automatic adaptation to light/dark themes.
typescript const tabs = [ { title: "Product", value: "product", content: ( <div className="w-full overflow-hidden relative rounded-2xl p-10 text-xl md:text-4xl font-bold text-foreground bg-muted h-[300px] border border-border"> <p>Product Tab</p> </div> ), }, // ... more tabs ];
These placeholder div panels can be replaced with any dynamic content your application requires.
The Tabs Component: Orchestrating State and Interaction
This component manages the active tab and orchestrates the interactive elements. Two key state variables drive its behavior:
typescript const [activeIdx, setActiveIdx] = useState(0); const [hovering, setHovering] = useState(false);
activeIdx: Tracks the currently selected tab's index.hovering: A boolean that signals whether the user's cursor is over any tab button, used to trigger the fan-out effect inFadeInStack.
Intelligent Tab Reordering for Stacked Content
A critical aspect of the stacked card effect lies in how tab content is rendered. Instead of conditionally rendering only the active tab's content, all tab panels are always rendered. The reorderedTabs array places the active tab at the first position:
typescript const reorderedTabs = [ tabs[activeIdx], ...tabs.filter((_, i) => i !== activeIdx), ];
This ensures the active panel is always at index 0, appearing on top with full scale and opacity, while inactive panels follow behind, progressively scaled down and faded.
Animated Tab Buttons with a Spring Pill
The tab buttons themselves incorporate a smooth, spring-animated indicator. The magic is in Framer Motion's layoutId prop:
typescript {tabs.map((tab, idx) => { const isActive = idx === activeIdx; return ( <button key={tab.value} onClick={() => handleSelect(idx)} onMouseEnter={() => setHovering(true)} onMouseLeave={() => setHovering(false)} className={cn("relative px-4 py-2 rounded-full", tabClassName)} style={{ transformStyle: "preserve-3d" }} > {isActive && ( <motion.div layoutId="clickedbutton" transition={{ type: "spring", bounce: 0.3, duration: 0.6 }} className={cn( "absolute inset-0 bg-primary rounded-full", activeTabClassName, )} /> )} <span className={cn( "relative block text-sm", isActive ? "text-background" : "text-foreground", )} > {tab.title} </span> </button> ); })}
When an element with layoutId="clickedbutton" unmounts from one button and mounts onto another, Framer Motion automatically animates its transition between the two DOM positions. The transition object defines a spring animation with a subtle bounce, mimicking physical momentum. The transformStyle: "preserve-3d" on the button, coupled with a perspective on the container, enables subtle 3D depth effects.
The FadeInStack Component: Dynamic Content Panels
The FadeInStack component renders the content panels, applying the stacked-card visuals and animations:
typescript const FadeInStack = ({ className, tabs, hovering }: FadeInStackProps) => { return ( <div className="relative w-full h-[300px]"> {tabs.map((tab, idx) => ( <motion.div key={tab.value} layoutId={tab.value} style={{ scale: 1 - idx * 0.1, top: hovering ? idx * -15 : 0, zIndex: -idx, opacity: idx < 3 ? 1 - idx * 0.1 : 0, }} animate={{ y: idx === 0 ? [0, 40, 0] : 0, }} className={cn("w-full h-full absolute top-0 left-0", className)} > {tab.content} </motion.div> ))} </div> ); };
Let's break down the key styling and animation logic within each motion.div:
scale: 1 - idx * 0.1: Each card behind the active one is scaled down by 10% per layer (e.g., active = 1.0, second = 0.9, third = 0.8), creating a clear depth separation.top: hovering ? idx * -15 : 0: Whenhoveringis true, cards shift upward, fanning out vertically by15pxper layer (the active card atidx=0remains stationary).zIndex: -idx: Negative z-index ensures correct stacking, with the active card (z-index 0) on top and subsequent cards progressively behind it.opacity: idx < 3 ? 1 - idx * 0.1 : 0: Cards at index 3 and beyond are hidden. The first three cards fade progressively (1.0, 0.9, 0.8).animate={{ y: idx === 0 ? [0, 40, 0] : 0 }}: Only the active card (idx === 0) performs a keyframe animation, dipping downward (y: 40) and bouncing back into place upon selection. This provides a subtle, tactile confirmation of the tab change.layoutId={tab.value}: Similar to the pill, each content card has alayoutId. WhenreorderedTabsshifts array positions, Framer Motion tracks each card's identity and animates its smooth transition to the new position, preventing abrupt jumps.
Integrating the Component
The top-level AnimatedTabMotion component wraps the Tabs component and sets the global perspective for 3D effects:
typescript export default function AnimatedTabMotion() { return ( <div className="[perspective:1000px] relative flex flex-col max-w-5xl mx-auto w-full items-start justify-start mb-13"> <Tabs tabs={tabs} /> </div> ); }
The [perspective:1000px] utility class from Tailwind (arbitrary value) sets the CSS perspective, which enables the 3D depth for elements with transformStyle: "preserve-3d".
Customizing Your Animated Tabs
The Tabs component's design allows for extensive customization. By passing specific className overrides, you can fully restyle its appearance to align with your project's design system.
typescript <Tabs tabs={tabs} containerClassName="gap-1" tabClassName="text-xs px-3 py-1.5" activeTabClassName="bg-zinc-900 dark:bg-white" contentClassName="mt-6" />
You can also replace the generic content panels with rich, functional components:
typescript const tabs = [ { title: "Overview", value: "overview", content: ( <div className="w-full rounded-2xl p-8 bg-muted border border-border h-[300px] flex flex-col gap-4"> <h2 className="text-2xl font-bold text-foreground">Product Overview</h2> <p className="text-muted-foreground text-sm leading-relaxed"> Our platform helps teams ship faster with a fully integrated design-to-code workflow. </p> </div> ), }, // ... ];
Key Concepts Recap
This component demonstrates powerful Framer Motion techniques for creating engaging UI:
| Technique | Description |
|---|---|
layoutId on motion.div | Animates a shared element between DOM positions (e.g., the sliding pill) |
layoutId per tab on motion.div | Tracks card identity during re-ordering for smooth position changes |
animate={{ y: [0, 40, 0] }} | Keyframe animation for a bounce entrance on tab change |
style={{ scale, top, zIndex, opacity }} | Inline reactive styles creating the stacked-card depth and hover effects |
transition={{ type: "spring" }} | Applies a physics-based spring curve for natural, elastic animations |
Conclusion
By combining Shadcn/ui's semantic design tokens with Framer Motion's robust animation capabilities, we've built a highly interactive and theme-aware tab component. This pattern, utilizing layoutId and array reordering, extends beyond tabs and can be applied to various UI elements like carousels, galleries, or notifications, enabling richer user experiences across your applications. Leveraging Shadcn Space further streamlines the integration of such production-quality components.
FAQ
Q: How does the sliding pill animation work without explicit position calculations?
A: The sliding pill animation is achieved using Framer Motion's layoutId prop. When a motion.div with a specific layoutId (e.g., "clickedbutton") unmounts from one element and mounts onto another in the DOM, Framer Motion automatically detects this change and animates its position and size between the old and new states. This eliminates the need for manual coordinate calculations.
Q: Why does this component render all tab content panels simultaneously instead of just the active one?
A: Rendering all tab content panels, with the active one reordered to the front, is fundamental to creating the stacked card and fan-out effects. It allows Framer Motion to track each content card's layoutId and animate its scale, top, zIndex, and opacity properties smoothly as its position in the reorderedTabs array changes. If only the active panel were rendered, these dynamic stacking and transition effects would not be possible.
Q: What is the purpose of transformStyle: "preserve-3d" on the tab buttons and [perspective:1000px] on the container?
A: These CSS properties enable 3D transformations for a subtle depth effect. [perspective:1000px] on the container establishes a 3D viewing context. For child elements, transformStyle: "preserve-3d" ensures that any 3D transformations (like implicit transformations applied by Framer Motion or even scale) are rendered in a 3D space, contributing to a more immersive and layered visual experience rather than a flat 2D movement.
Related articles
TCG Pocket Responds: A Much-Needed Pace Shift for Pokémon Fans
Pokémon TCG Pocket is rolling out a major update for its second anniversary, primarily addressing player concerns about overwhelming content releases by shifting to a six-week set schedule. This aims to curb player decline and improve overall engagement by giving trainers more time to collect and strategize. The update also adds new collection methods, improved deck-building features, and enhanced trading functionality.
AI's Confidence vs. Formal Methods' Rigor: Design Flaws & Verification
While AI accelerates code generation, formal methods remain critical for ensuring system correctness. Languages like TLA+ and tools like Quint enable developers to model and verify system designs before coding, catching subtle flaws. Quint makes formal methods more accessible with developer-friendly syntax and executability. This approach is vital for verifying AI-generated code through trace validation and model-based testing, ensuring reliability against real-world data.
Ephemeral PR Preview Environments on GKE: A Deep Dive
Every development team eventually grapples with the limitations of shared staging environments. The familiar scenario: multiple features deployed simultaneously, database migrations clashing, and the phrase "it works on
Secure Internal API Access for AI Agents with MCP
AI agents accessing internal APIs directly pose significant risks, including identity loss, unrestricted queries, and security bypasses. The Model Context Protocol (MCP) offers a solution by defining explicit tools. A secure architecture involves an MCP server wrapping the API with strict input validation, coupled with a gateway that handles authentication, rate limiting, and audit logging, ensuring safe and accountable access.
Beyond the Resume Noise: Developer Story Rethinks Tech Hiring
Stack Overflow is addressing the growing challenges in tech hiring, exacerbated by AI's influence leading to untrustworthy and generic resumes. Their solution, the re-introduced Developer Story, offers a platform for developers to authentically showcase their work, expertise, and impact through verifiable contributions and personal projects. This aims to provide hiring teams with more objective insights and help genuine talent stand out in a crowded market.
Adding & Validating JSON-LD Structured Data in Next.js
When we build websites, we want them to be discoverable and accurately understood by search engines. This isn't just about keywords and meta descriptions anymore; it's about providing explicit, machine-readable context.






