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
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>
);
}
Installation
Install the dependencies
$ npm i motion lucide-reactCopy the source into
components/ui/animated-tabs.tsxcomponents/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> ); }Use it
The file exports the component and a
Demoshowing typical usage. Import whichever you need.