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
Google Play's New Stance on 501(c)(6) Donations: AnkiDroid's Challenge
For developers deeply embedded in the open-source ecosystem, the challenge of sustainable funding is ever-present. Many projects rely on community donations, often facilitated by fiscal hosts that simplify legal and
Cold Cases & Data Integrity: Lessons from a Decades-Old Verdict
As software developers, we often deal with complex systems, legacy codebases, and the relentless pursuit of bugs that have evaded detection for years. The recent conviction in the 1996 murder of rapper Tupac Shakur
Coyote vs. Acme: A Hilarious, Heartfelt Tribute to Looney Tunes
After a protracted journey to the big screen, "Coyote vs. Acme" emerges not just as a film, but as a vibrant celebration of classic animation. This live-action and animated hybrid stands as an irresistible blend of
ai: Musk’s faster path to more gas turbines comes with pollution
Elon Musk's SpaceX is building a secret Texas foundry to produce gas turbine blades, aiming to accelerate AI data center power by 18 months. This addresses a critical energy bottleneck, but faces environmental backlash over pollution and health risks from gas turbines.
Reimagining Classic IM: Exploring Open OSCAR Server in Go
Open OSCAR Server is an open-source, Go-based instant messaging server compatible with classic AIM and ICQ clients. It enables developers and enthusiasts to self-host a private IM server, reviving the functionality of these legacy platforms. The project boasts broad client compatibility, detailed protocol implementations, and a management API for administration.
Startup Spotlight: Skyfarer Connects Pilots with Aviation Services
Skyfarer, a startup founded by Nick Tsang in Camas, Wash., is building an "Airbnb for aviation," a unified online marketplace connecting pilots with flight training, instructors, mechanics, and aircraft. Addressing a fragmented multi-billion dollar general aviation market, Skyfarer has rapidly grown to nearly 14,000 users and 8,500 listings by offering essential services and leveraging AI for swift development. The company's vision is to support pilots throughout their entire aviation journey, aiming to reduce dropout rates and foster a thriving flying community.



