News Froggy
newsfroggy
HomeTechReviewProgrammingGamesHow ToAboutContacts
newsfroggy

Your daily source for the latest technology news, startup insights, and innovation trends.

More

  • About Us
  • Contact
  • Privacy Policy
  • Terms of Service

Categories

  • Tech
  • Review
  • Programming
  • Games
  • How To

© 2026 News Froggy. All rights reserved.

TwitterFacebook
Programming

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.

PublishedMarch 31, 2026
Reading Time12 min
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. 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 in FadeInStack.

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: When hovering is true, cards shift upward, fanning out vertically by 15px per layer (the active card at idx=0 remains 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 a layoutId. When reorderedTabs shifts 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:

TechniqueDescription
layoutId on motion.divAnimates a shared element between DOM positions (e.g., the sliding pill)
layoutId per tab on motion.divTracks 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.

#programming#freeCodeCamp#UI#shadcn#Web Development#buildingMore

Related articles

Programming
Hacker NewsJun 2

Great Question (YC W21) Seeks Applied AI Interns: A Deep Dive

As fellow developers, we’re constantly scanning the landscape for companies pushing the boundaries, especially in the rapidly evolving AI space. Great Question, a Y Combinator W21 alumnus, has caught our eye with an

Navigating the Global AI Arena: Beyond Silicon Valley's Borders
Programming
Stack Overflow BlogJun 2

Navigating the Global AI Arena: Beyond Silicon Valley's Borders

The international AI landscape presents unique challenges and opportunities, requiring developers to think beyond traditional tech hubs. Key aspects include adapting AI models to local languages and cultures, navigating the complex global supply chain for critical hardware like semiconductors, and understanding how venture capital assesses these international ventures. Success hinges on deep local market understanding, robust technical solutions for localization, and resilience against logistical hurdles.

Programming
Hacker NewsJun 2

Engineering a Solution: Debugging Global Mosquito-Borne Diseases

As developers, we're constantly tasked with solving complex problems, whether it's optimizing a database query or architecting a distributed system. But what if the 'bug' we're trying to fix is biological, with global

Self-Host S3-Compatible Object Storage with MinIO on Staging
Programming
freeCodeCampJun 2

Self-Host S3-Compatible Object Storage with MinIO on Staging

This guide demonstrates how to self-host an S3-compatible object store using MinIO on your staging server. By leveraging Docker Compose and Traefik for HTTPS, you can significantly reduce cloud storage costs while maintaining a production-like environment for development and testing. It covers setup, application configuration, and secure file interactions.

Programming
Hacker NewsJun 1

Unleashing LLMs: A 10-Year-Old Xeon is All You Need

This article explores how a 10-year-old Intel Xeon E5-2620 v4 server with 128 GB DDR3 RAM and no GPU can run a modern LLM like Gemma 4 26B-A4B at reading speed. It highlights that LLM inference is often memory-bound and showcases deep optimization techniques using `ik_llama.cpp`, including speculative decoding, CPU-aware MoE routing, advanced memory management, and specialized attention kernels. The success demonstrates that granular software control can unlock significant performance on older, abundant-RAM hardware.

Secluso: Building Private Home Security on Raspberry Pi with E2EE
Programming
Hacker NewsMay 30

Secluso: Building Private Home Security on Raspberry Pi with E2EE

Reclaiming Privacy in Home Security with Secluso For many developers, the allure of smart home technology, including security cameras, is strong. Yet, the widespread reliance on cloud-based services for video storage

Back to Newsroom

Stay ahead of the curve

Get the latest technology insights delivered to your inbox every morning.