How to create an animated Shadcn Tab component with Shadcn/ui

by SkillAiNest

Tab components are everywhere: dashboards, settings panels, product pages. But most implementations are static, lifeless and forgettable. What if your tabs felt alive, with smooth spring animations, a stacked card effect on hover, and a shiny active indicator that glides between buttons?

A basic tab switcher can show and hide content. A better one gives the user a clear active state, smooth transitions, and slight movement that makes the interface feel alive. That’s the idea behind this component: a reusable animated tab system built in Shadcn style with React, Tailwind CSS, and Motion.

In this tutorial, you’ll create exactly that: a fully animated tab component of a ready-to-use registry with Shadcn/ui, Framer Motion, and Shadcn Space.

By the end, you will have a reusable. Component with:

  • Spring active tablet indicator

  • A stacked card effect that fans out on hover.

  • A smooth entry animation when the active tab changes

  • Fully theme-aware styling using Shadcn/ui CSS variables

Video walkthrough: If you prefer to follow along visually, watch the full tutorial on YouTube:

Table of Contents

Conditions

Before you begin, make sure you have a working knowledge of:

  • Basics of React and TypeScript

  • Tailwind CSS utility classes

  • Shadcn/ui Basics (Component Installation and Theming)

You’ll also need a Next.js or Vite project with the following preconfigured:

What will you make?

Here’s an overview of the component architecture you’ll build in this tutorial:

AnimatedTabMotion (page/demo entry point)
└── Tabs (tab bar + content orchestrator)
├── Tab buttons (with spring-animated active pill)
└── FadeInStack (stacked, animated content panels)

The key behaviors are:

  1. Spring bullet animation. – A spring pill animation is a UI effect in which the active tab indicator, a round, bullet-shaped highlight, physically moves from button to button using a spring physics curve instead of a standard CSS transition. Instead of teleporting or fading, the bullet slides between the tabs with a subtle bounce at the end, mimicking the motion of a real physical object.

  2. The effect of a stacked card – Passive tab panels are projected behind active ones, smaller and slightly faded at the bottom, giving the illusion of layered depth.

  3. Fan out on the hoar – When the user hovers over the content area, the stacked cards expand vertically.

  4. Bounce entry – When a new tab is selected, the top (active) card moves down and back into place.

Install the component via the Shadcn Space CLI.

Shadcn Space is a registry of production-ready Shadcn/ui-compatible components. Instead of scaffolding this component from scratch, you can pull it directly into your project using the Shadcn CLI.

Check them out Getting Started Guide To learn how to use the Shadcn CLI with third-party registries.

run one Depending on your package manager, either of the following commands:

pnpm

pnpm dlx shadcn@latest add @shadcn-space/tabs-01

NPM

npx shadcn@latest add @shadcn-space/tabs-01

yarn

yarn dlx shadcn@latest add @shadcn-space/tabs-01

become

bunx --bun shadcn@latest add @shadcn-space/tabs-01

This scaffolds a component file into your project, pre-wired to your existing Shadcn/ui theme tokens. Then you can customize or extend it as needed, which is exactly what you’ll learn in this tutorial.

Understand the structure of components.

Before writing any code, let’s review the entire component and break it down into logical chunks. Here is the full implementation:

"use client";

import { useState } from "react";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";

type Tab = {
  title: string;
  value: string;
  content?: React.ReactNode;
};

type TabsProps = {
  tabs: Tab();
    containerClassName?: string;
  activeTabClassName?: string;
  tabClassName?: string;
  contentClassName?: string;
};

const tabs = (
  {
    title: "Product",
    value: "product",
    content: (
      
    ),
  },
  {title: "Services",
    value: "services",
    content: (
      
    ),
  },
  {
    title: "Playground",
    value: "playground",
    content: (
      
    ),
  },
 {
    title: "Content",
    value: "content",
    content: (
      
    ),
  },
  {
    title: "Random",
    value: "random",
    content: (
      
    ),
  },
);

const Tabs = ({
  tabs,
  containerClassName,
  activeTabClassName,
  tabClassName,
  contentClassName,
}: TabsProps) => {
  const (activeIdx, setActiveIdx) = useState(0);
  const (hovering, setHovering) = useState(false);

  const handleSelect = (idx: number) => {
    setActiveIdx(idx);
  };
const reorderedTabs = (
    tabs(activeIdx),
    ...tabs.filter((_, i) => i !== activeIdx),
  );

  return (
    <>
      

{tabs.map((tab, idx) => { const isActive = idx === activeIdx; return ( ); })}

> ); }; type FadeInStackProps = { className?: string; tabs: Tab(); hovering?: boolean; }; const FadeInStack = ({ className, tabs, hovering }: FadeInStackProps) => { return (

{tabs.map((tab, idx) => ( {tab.content} ))}

); }; export default function AnimatedTabMotion() { return ( <> > ); }

Now, let’s break it down into pieces.

Step 1: Define tab data types.

type Tab = {
title: string;
value: string;
content?: React.ReactNode;
};
type TabsProps = {
tabs: Tab();
containerClassName?: string;
activeTabClassName?: string;
tabClassName?: string;
contentClassName?: string;
};

gave Tab type defines the format of each tab item:

  • title – Label presented in tab button.

  • value – A unique key used to identify each tab (and as framer motion layoutId).

  • content – An optional. React.ReactNodeThat is, you can pass any JSX as the panel body.

gave TabsProps Makes type Tabs Highly mixed ingredients. Each visual layer has an override. classNameso you can freely rearrange the active tablet, individual tab buttons, and content area without touching the underlying logic.

Step 2: Construct the tab data array.

const tabs = (
{
title: “Product”,
value: “product”,
content: (

Product Tab

), }, // ... more tabs );

of each tab content Shadcn/ui is a semantic token-like JSX element. bg-muted, text-foreground And border-border. This is intentional: these tokens automatically adapt to your light/dark theme without any additional configuration.

You can change these placeholders.

panels with any real content: charts, forms, tables, media, whatever your use case demands.

Step 3: Build the Tabs Component (Tab Bar + State)

const (activeIdx, setActiveIdx) = useState(0);
const (hovering, setHovering) = useState(false);

Two pieces of state drive the entire component:

  • activeIdx Tracks which tab is currently selected (by array index).

  • hovering Tracks whether the user's cursor is on any tab button, which is passed. FadeInStack To activate the fan-out effect.

Rearrange the tabs for a stacked effect.

const reorderedTabs = (
tabs(activeIdx),
…tabs.filter((_, i) => i !== activeIdx),
);

This is one of the cleverest aspects of architecture. Instead of just showing the content of the active tab, you Always render all tab panels. - But you put Active in the first row. This is what enables stacked cards:

  • Index 0 = active panel, rendered above with full scale and opacity.

  • Index 1, 2 = next panels, held back with reduced scale and opacity.

  • Index 3+ = invisible (Opacity 0).

Render tab buttons with spring bullet.

{tabs.map((tab, idx) => {
const isActive = idx === activeIdx;
return (
   
);
})}

The magic is here. layoutId=“clickedbutton” But motion.div. When only one factor with given layoutId Installed one at a time, Framer Motion tracks its position in the DOM. When it unmounts from one button and mounts on another, Framer Motion automatically animates the transition is between two DOM positions. It creates a sliding bullet effect with zero manual calculations.

Used with a spring in a transfer configuration. bounce: 0.3 a duration: 0.6giving it a natural, slightly flexible feel rather than a mechanical linear slide.

gave transformStyle: “preserve-3d” Enables 3D CSS transforms on the button, which combines with (perspective:1000px) on the container for a subtle depth effect.

Step 4: Create the FadeInStack component

const FadeInStack = ({ className, tabs, hovering }: FadeInStackProps) => {
  return (
    

{tabs.map((tab, idx) => ( {tab.content} ))}

); };

Let's unpack visual logic for everyone. motion.div:

scale: 1 - idx * 0.1

Each card behind the active card is shortened by 10% per layer. So:

  • Active card (idx 0): scale: 1.0

  • Second card (idx 1): scale: 0.9

  • Third card (idx 2): scale: 0.8

This creates a clear depth separation between the stacked layers.

top: hovering ? idx * -15 : 0

when hovering is trueeach card shifts up. idx * 15px. The active card does not move (idx 15 = 0)but the cards behind it fan out at -15px, -30px, etc. This gives a satisfying "deck spread" effect on the hover.

zIndex: -idx

A negative z-index stacks the cards in order: the active card sits on top (z-index 0), while subsequent cards come further back.

opacity: idx < 3 ? 1 - idx * 0.1 : 0

Cards with index 3 and beyond are completely invisible. The first three cards fade out gradually: 1.0, 0.9, 0.8.

animate={{ y: idx === 0 ? (0, 40, 0) : 0 }}

Only the active card (idx 0) gets this keyframe animation. When a tab is selected, and reorderedTabs The array is rearranged, with the new active card inserted from the bottom (y: 40) and bounces back to its resting position. This is a quick, tactile confirmation that the tab has changed.

layoutId={tab.value}

Each card also has one. layoutId A matching one value. when reorderedTabs As recounts are performed, and array positions shift, Framer Motion can track each card's identity and move it smoothly between positions, preventing clashes.

Step 5: Compose the page component.

export default function AnimatedTabMotion() {
  return (
    
  );
}

The outer wrapper is applied. (perspective:1000px) - An optional property of tailwind that sets the CSS. perspective Value This is what gives 3D depth transformStyle: “preserve-3d” On the tab buttons.

gave max-w-5xl And mx-auto Place the component on wide screens while items-start Aligns the tab bar to the left, which matches most real-world UI patterns.

Step 6: Customize the Components

Because Tabs Accepts class name overrides for each visual layer, so you can completely restyle a component to match your design system. Here's an example with a deeply active tablet and a strict setting:


You can also replace placeholder content panels with real content. Here's an example of using a card with a real description:

const tabs = (
  {
    title: "Overview",
    value: "overview",
    content: (
      

Product Overview

Our platform helps teams ship faster with a fully integrated design-to-code workflow.

), }, // ... );

Live preview

af4a2ba6-dd70-4e77-8c38-7d390060db0d

Summary of key concepts

Here is a summary of the basic framer motion techniques used in this component:

The technique

What does it do?

layoutId But motion.div

Activates the shared element between DOM positions (sliding bullet).

layoutId But motion.div per tab

Tracks card identity during reset, so Framer Motion triggers position changes.

animate={{ y: (0, 40, 0) }}

Keyframe animation for bounce entry on tab change

style={{ scale, top, zIndex, opacity }}

Inline reactive styles that create a stacked card depth effect.

transition={{ type: "spring" }}

CSS applies a physics-based spring curve instead of an easing function.

The result

In this tutorial, you've created a fully animated, theme-aware tab component using Shadcn/ui and Framer Motion. You learned how to:

  • use layoutId To create a spring-animated sliding bullet indicator

  • Render all tab panels simultaneously and rearrange them to create a stacked card effect.

  • Drive hover and depth effects with inline reactive style support

  • Apply FramerMotion's frame animations for tactile bounce input.

  • Fully customize the component through class name overrides

This pattern, combining Shadcn/ui's semantic design tokens with Framer Motion's layout animations, scales well beyond tabs. You can apply the same. layoutId And stack reordering techniques into carousels, image galleries, notification toasts, and more.

You can find complete components and more dynamic UI blocks in Shadcn Space, where CLI commands make it trivial to drop production-quality components directly into your project.

Resources

I have written this article with the help of Meher Koshti (Senior Full Stack Developer) – Connect on LinkedIn..



If you read this far, thank the author for letting them know you care.

Learn to code for free. freeCodeCamp's open source curriculum has helped more than 40,000 people land jobs as developers. start

You may also like

Leave a Comment

At Skillainest, we believe the future belongs to those who embrace AI, upgrade their skills, and stay ahead of the curve.

Get latest news

Subscribe my Newsletter for new blog posts, tips & new photos. Let's stay updated!

@2025 Skillainest.Designed and Developed by Pro