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
Building Responsive, Accessible React UIs with Semantic HTML
Build responsive and accessible React UIs. This guide uses semantic HTML, mobile-first design, and ARIA to create inclusive applications, ensuring seamless user experiences across devices.
Artemis II: Wholesome Space Content Saves the Internet from
The Artemis II mission is providing a much-needed dose of wholesome content to a cynical internet. From emotional tributes to a viral Nutella escape and a space-themed sitcom intro, astronauts are sharing genuine, feel-good moments.
Beyond Vibe Coding: Engineering Quality in the AI Era
The concept of 'vibe coding,' an extreme form of dogfooding where developers avoid inspecting AI-generated code, often leads to significant quality issues. A more effective approach involves actively guiding AI tools to clean up technical debt and refactor, treating them as powerful assistants under human oversight. Ultimately, maintaining high software quality, even with AI, remains a deliberate choice for developers.
Offline-First Social Systems: The Rise of Phone-Free Venues
Mobile technology, while streamlining communication and access, has also ushered in an era of constant digital distraction. For developers familiar with context switching and notification fatigue, the impact on
Big Three Carriers in 2026: Still Worth It? It's Complicated
As we navigate further into 2026, the landscape of mobile wireless continues its rapid evolution. Verizon is actively trying to improve its image with special loyalty offers and promotions, T-Mobile revamped its plans
Europe's Tech Funding: AI, Quantum & Infrastructure Lead the Week
Europe's tech sector saw substantial funding from March 30-April 5, led by Mistral AI's $830M debt for AI compute. The week highlighted a strategic European focus on building foundational infrastructure across AI, quantum, and deep tech, aiming for increased technological autonomy and global influence.





