Installation
npx shadcn@latest add https://tentui.com/r/animated-tabs.json
Usage
Pass an array of { value, label } objects. The component manages selection state internally — pass value and onValueChange if you want to control it yourself.
import { AnimatedTabs } from "@/components/animated-tabs";
const tabs = [
{ value: "overview", label: "Overview" },
{ value: "analytics", label: "Analytics" },
{ value: "reports", label: "Reports" }
];
export function Example() {
return <AnimatedTabs tabs={tabs} defaultValue="overview" />;
}Patterns
Controlled selection
Bind value to your own state when the active tab needs to drive other UI — a panel, a query string, an analytics event.
"use client";
import { useState } from "react";
import { AnimatedTabs } from "@/components/animated-tabs";
export function FilterRow() {
const [tab, setTab] = useState("all");
return (
<AnimatedTabs
tabs={[
{ value: "all", label: "All" },
{ value: "open", label: "Open" },
{ value: "closed", label: "Closed" }
]}
value={tab}
onValueChange={setTab}
/>
);
}Multiple groups on one page
The sliding indicator is driven by Framer Motion's layoutId. If you render two AnimatedTabs groups on the same page, give each one a unique layoutId — otherwise the indicator will animate between them across groups.
<AnimatedTabs tabs={primary} layoutId="primary-tabs" />
<AnimatedTabs tabs={secondary} layoutId="secondary-tabs" />Props
| Prop | Type | Default | Description |
|---|---|---|---|
tabs | { value, label }[] | — | Tabs to render. label accepts any ReactNode — strings, icons, fragments. |
value | string | — | Controlled active tab value. Pair with onValueChange. |
defaultValue | string | tabs[0].value | Initial active tab when uncontrolled. |
onValueChange | (value: string) => void | — | Fires when the user picks a different tab. |
layoutId | string | "animated-tabs-underline" | motion layout id for the indicator. Override when multiple instances render on the same page. |
className | string | — | Forwarded to the root <div>. |
Accessibility
The root carries role="tablist" and each trigger is a real <button role="tab"> with aria-selected. Use Tab/Shift+Tab to move focus, and Enter or Space to activate. If you wire up associated panels, give each panel role="tabpanel" and link it to its trigger via aria-controls/id.