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
Build Your Own Local NMT App with React Native and QVAC
This article explores how Neural Machine Translation (NMT), powered by the Transformer architecture, revolutionized translation by understanding context. We then delve into QVAC, a local-first AI development platform, and its Bergamot engine, enabling private, on-device translation. Learn to set up a React Native app with QVAC and manage model lifecycles for efficient local translation.
Unpacking Roman Concrete's Durability: Carbonation and Self-Healing
The Enduring Legacy: Roman Concrete's Millennia-Long Stand As software developers, we're familiar with the ephemeral nature of technology; systems evolve, frameworks deprecate, and codebases undergo constant
PayPal in Microservices: NestJS, gRPC, and Docker Blueprint
Integrating payment logic directly into every microservice within a distributed system often leads to significant challenges. Scattering PayPal API calls across services like user-service, order-service, or
Demystifying Dijkstra's Algorithm: The Shortest Path Pioneer
Explore Dijkstra's Algorithm, the foundational pathfinding technique conceived by Edsger W. Dijkstra. This guide explains how it solves shortest path problems using graphs, nodes, edges, and weights. Learn its greedy approach and the critical role of data structures like adjacency lists and priority queues in its efficient Python implementation.
Applied Computing wants to give oil and gas operators an AI model for
Applied Computing, a London-based startup, has secured $20 million in Series A funding to advance its foundation AI model, Orbital, for the oil, gas, and petrochemical industry. Orbital aims to integrate disparate data sources—sensor readings, engineering data, and physics models—to provide real-time operational insights, drastically reducing investigation times and enhancing efficiency. The company plans to use the capital for international expansion, hiring, and new client deployments, building on its rapid growth and strategic partnerships with industry giants like KBR.
AWS Leadership Shift: What It Means for Compute and AI/ML
Dave Brown, a key figure in AWS's EC2 and AI/ML growth, is departing. His successor, Dave Treadwell, brings extensive experience from Microsoft and Amazon's eCommerce Foundation, potentially signaling new directions for core cloud services and AI innovation.





