Animated Tabs

Sliding pill active indicator tab switcher with Framer Motion layoutId and animated panel transitions.

ReactTailwind CSSFramer Motion
Dependenciesframer-motion, lucide-react
npm install framer-motion lucide-react

Preview

Overview Panel

Explore real-time telemetry, core performance metrics, and activity history in a unified canvas.

Source Code

animated-tabs.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
"use client";

import React, { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Code2, Eye, FileText, Cpu, LucideIcon } from "lucide-react";

export interface TabItem {
  id: string;
  label: string;
  icon?: LucideIcon;
  badge?: string;
  content: string;
}

export function AnimatedTabs({ tabs }: { tabs?: TabItem[] }) {
  const defaultTabs: TabItem[] = [
    { id: "overview", label: "Overview", icon: Eye, content: "Unified canvas telemetry." },
    { id: "source", label: "Source", icon: Code2, badge: "TSX", content: "Self-contained component definitions." },
  ];
  const items = tabs || defaultTabs;
  const [activeTab, setActiveTab] = useState(items[0].id);

  return (
    <div className="w-full max-w-lg mx-auto space-y-4">
      <div className="flex items-center gap-1 p-1 rounded-xl bg-zinc-100 dark:bg-zinc-900 border border-zinc-200/80 dark:border-zinc-800/80">
        {items.map((tab) => {
          const Icon = tab.icon;
          const isActive = activeTab === tab.id;

          return (
            <button
              key={tab.id}
              onClick={() => setActiveTab(tab.id)}
              className={`relative flex-1 flex items-center justify-center gap-1.5 py-2 px-3 rounded-lg text-xs font-medium transition-colors cursor-pointer select-none
                ${isActive ? "text-zinc-900 dark:text-zinc-100" : "text-zinc-500 hover:text-zinc-800 dark:hover:text-zinc-300"}`}
            >
              {isActive && (
                <motion.div
                  layoutId="active-tab-pill"
                  className="absolute inset-0 bg-white dark:bg-zinc-800 rounded-lg shadow-sm border border-zinc-200/50 dark:border-zinc-700/50"
                  transition={{ type: "spring", stiffness: 450, damping: 32 }}
                />
              )}
              <span className="relative z-10 flex items-center gap-1.5">
                {Icon && <Icon className="w-3.5 h-3.5 opacity-80" />}
                <span>{tab.label}</span>
              </span>
            </button>
          );
        })}
      </div>
    </div>
  );
}

Usage

usage.tsx
1
2
3
4
5
6
<AnimatedTabs
  tabs={[
    { id: "preview", label: "Preview", icon: Eye, content: "Live canvas" },
    { id: "code", label: "Code", icon: Code2, content: "TSX source" },
  ]}
/>

Props

PropTypeRequiredDefaultDescription
tabsTabItem[]OptionaldefaultTabsArray of tab descriptors with id, label, icon, badge, and content.

Types

types.ts
1
2
3
4
5
6
7
export interface TabItem {
  id: string;
  label: string;
  icon?: LucideIcon;
  badge?: string;
  content: string;
}