Animated Tabs

Segmented tabs with a shared-layout indicator and blur crossfade.

Understand every user

Funnels, retention and cohorts built automatically from your events.

+32%

avg. improvement

Installation

  1. Install the dependencies

    $ npm i motion lucide-react
  2. Copy the source into components/ui/animated-tabs.tsx

    components/ui/animated-tabs.tsx
    "use client";
    
    import { AnimatePresence, motion } from "motion/react";
    import { BarChart3, MessageSquare, Workflow } from "lucide-react";
    import { useId, useState, type ReactNode } from "react";
    
    type Tab = { id: string; label: string; icon?: ReactNode; content: ReactNode };
    
    export function AnimatedTabs({ tabs }: { tabs: Tab[] }) {
      const [active, setActive] = useState(tabs[0].id);
      const layoutId = useId();
      const current = tabs.find((t) => t.id === active) ?? tabs[0];
    
      return (
        <div className="w-full">
          <div role="tablist" className="inline-flex gap-1 rounded-full border border-zinc-200 bg-zinc-50 p-1">
            {tabs.map((tab) => (
              <button
                key={tab.id}
                role="tab"
                aria-selected={active === tab.id}
                onClick={() => setActive(tab.id)}
                className={`relative flex items-center gap-2 rounded-full px-4 py-2 text-sm font-medium transition-colors ${active === tab.id ? "text-white" : "text-zinc-600 hover:text-zinc-900"}`}
              >
                {active === tab.id && (
                  <motion.span
                    layoutId={layoutId}
                    className="absolute inset-0 rounded-full bg-zinc-900"
                    transition={{ type: "spring", stiffness: 380, damping: 30 }}
                  />
                )}
                <span className="relative flex items-center gap-2">
                  {tab.icon}
                  {tab.label}
                </span>
              </button>
            ))}
          </div>
          <div className="relative mt-6">
            <AnimatePresence mode="wait">
              <motion.div
                key={current.id}
                role="tabpanel"
                initial={{ opacity: 0, y: 10, filter: "blur(4px)" }}
                animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
                exit={{ opacity: 0, y: -10, filter: "blur(4px)" }}
                transition={{ duration: 0.3 }}
              >
                {current.content}
              </motion.div>
            </AnimatePresence>
          </div>
        </div>
      );
    }
    
    function Panel({ title, text, stat }: { title: string; text: string; stat: string }) {
      return (
        <div className="grid gap-6 rounded-2xl border border-zinc-200 bg-white p-7 sm:grid-cols-[1fr_auto] sm:items-center">
          <div>
            <h3 className="text-lg font-semibold text-zinc-900">{title}</h3>
            <p className="mt-2 max-w-md text-sm leading-relaxed text-zinc-500">{text}</p>
          </div>
          <div className="rounded-xl bg-blue-50 px-5 py-4 text-center">
            <p className="text-2xl font-semibold text-blue-700">{stat}</p>
            <p className="text-xs text-blue-600/70">avg. improvement</p>
          </div>
        </div>
      );
    }
    
    export default function Demo() {
      return (
        <div className="w-full max-w-2xl">
          <AnimatedTabs
            tabs={[
              { id: "analytics", label: "Analytics", icon: <BarChart3 className="size-4" />, content: <Panel title="Understand every user" text="Funnels, retention and cohorts built automatically from your events." stat="+32%" /> },
              { id: "automation", label: "Automation", icon: <Workflow className="size-4" />, content: <Panel title="Workflows that run themselves" text="Trigger emails, alerts and syncs the moment something changes." stat="12h/wk" /> },
              { id: "support", label: "Support", icon: <MessageSquare className="size-4" />, content: <Panel title="Answers in seconds" text="A shared inbox with AI-suggested replies and full customer context." stat="-48%" /> },
            ]}
          />
        </div>
      );
    }
    
  3. Use it

    The file exports the component and a Demo showing typical usage. Import whichever you need.