Tab navigation with underline, background, and ghost variants, and an indicator that follows the shape system
Manage your account settings.
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";const PANELS = [{ value: "account", copy: "Manage your account settings." },{ value: "password", copy: "Change your password here." },{ value: "settings", copy: "Configure your preferences." },] as const;const LABELS: Record<string, string> = {account: "Account",password: "Password",settings: "Settings",};export default function TabsDemo() {return (<Tabs defaultValue="account" size="lg" className="w-fit max-w-full"><TabsList>{PANELS.map((panel) => (<TabsTrigger key={panel.value} value={panel.value}>{LABELS[panel.value]}</TabsTrigger>))}</TabsList><div className="relative min-h-[24px]">{PANELS.map((panel) => (<TabsContentkey={panel.value}value={panel.value}className="absolute inset-x-0 top-0"><p className="text-caption text-muted-foreground">{panel.copy}</p></TabsContent>))}</div></Tabs>);}
Installation
npx shadcn@latest add https://www.deltacomponents.dev/r/tabs.json
Usage
1import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";23<Tabs defaultValue="account">4 <TabsList>5 <TabsTrigger value="account">Account</TabsTrigger>6 <TabsTrigger value="password">Password</TabsTrigger>7 </TabsList>8 <TabsContent value="account">Manage your account settings.</TabsContent>9 <TabsContent value="password">Change your password here.</TabsContent>10</Tabs>
Basic
Manage your account settings.
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";const PANELS = [{ value: "account", copy: "Manage your account settings." },{ value: "password", copy: "Change your password here." },{ value: "settings", copy: "Configure your preferences." },] as const;const LABELS: Record<string, string> = {account: "Account",password: "Password",settings: "Settings",};export default function TabsDemo() {return (<Tabs defaultValue="account" size="lg" className="w-fit max-w-full"><TabsList>{PANELS.map((panel) => (<TabsTrigger key={panel.value} value={panel.value}>{LABELS[panel.value]}</TabsTrigger>))}</TabsList><div className="relative min-h-[24px]">{PANELS.map((panel) => (<TabsContentkey={panel.value}value={panel.value}className="absolute inset-x-0 top-0"><p className="text-caption text-muted-foreground">{panel.copy}</p></TabsContent>))}</div></Tabs>);}
Underline
Manage your account settings.
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";const PANELS = [{ value: "account", copy: "Manage your account settings." },{ value: "password", copy: "Change your password here." },{ value: "settings", copy: "Configure your preferences." },] as const;const LABELS: Record<string, string> = {account: "Account",password: "Password",settings: "Settings",};export default function TabsUnderline() {return (<Tabs defaultValue="account" variant="underline" size="lg" className="w-fit max-w-full"><TabsList>{PANELS.map((panel) => (<TabsTrigger key={panel.value} value={panel.value}>{LABELS[panel.value]}</TabsTrigger>))}</TabsList><div className="relative min-h-[24px]">{PANELS.map((panel) => (<TabsContentkey={panel.value}value={panel.value}className="absolute inset-x-0 top-0"><p className="text-caption text-muted-foreground">{panel.copy}</p></TabsContent>))}</div></Tabs>);}
Ghost
Manage your account settings.
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";const PANELS = [{ value: "account", copy: "Manage your account settings." },{ value: "password", copy: "Change your password here." },{ value: "settings", copy: "Configure your preferences." },] as const;const LABELS: Record<string, string> = {account: "Account",password: "Password",settings: "Settings",};export default function TabsGhost() {return (<Tabs defaultValue="account" variant="ghost" size="lg" className="w-fit max-w-full"><TabsList>{PANELS.map((panel) => (<TabsTrigger key={panel.value} value={panel.value}>{LABELS[panel.value]}</TabsTrigger>))}</TabsList><div className="relative min-h-[24px]">{PANELS.map((panel) => (<TabsContentkey={panel.value}value={panel.value}className="absolute inset-x-0 top-0"><p className="text-caption text-muted-foreground">{panel.copy}</p></TabsContent>))}</div></Tabs>);}
Sizes
Manage your account settings.
import { useState } from "react";import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";const PANELS = [{ value: "account", copy: "Manage your account settings." },{ value: "password", copy: "Change your password here." },{ value: "settings", copy: "Configure your preferences." },] as const;const LABELS: Record<string, string> = {account: "Account",password: "Password",settings: "Settings",};export default function TabsSizes() {const [size, setSize] = useState<"sm" | "default" | "lg">("lg");return (<div className="flex w-fit max-w-full flex-col items-start gap-5"><Tabs defaultValue="account" size={size}><TabsList>{PANELS.map((panel) => (<TabsTrigger key={panel.value} value={panel.value}>{LABELS[panel.value]}</TabsTrigger>))}</TabsList><div className="relative min-h-[24px]">{PANELS.map((panel) => (<TabsContentkey={panel.value}value={panel.value}className="absolute inset-x-0 top-0"><p className="text-caption text-muted-foreground">{panel.copy}</p></TabsContent>))}</div></Tabs>
With Icons
Manage your account settings.
import { useIcon } from "@/lib/icon-context";import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";const PANELS = [{ value: "account", copy: "Manage your account settings." },{ value: "password", copy: "Change your password here." },{ value: "settings", copy: "Configure your preferences." },] as const;export default function TabsWithIcons() {const UserIcon = useIcon("user");const LockIcon = useIcon("lock");const SettingsIcon = useIcon("settings");return (<Tabs defaultValue="account" size="lg" className="w-fit max-w-full"><TabsList><TabsTrigger value="account" icon={<UserIcon />}>Account</TabsTrigger><TabsTrigger value="password" icon={<LockIcon />}>Password</TabsTrigger><TabsTrigger value="settings" icon={<SettingsIcon />}>Settings</TabsTrigger></TabsList><div className="relative min-h-[24px]">{PANELS.map((panel) => (<TabsContentkey={panel.value}value={panel.value}className="absolute inset-x-0 top-0"><p className="text-caption text-muted-foreground">{panel.copy}</p></TabsContent>))}</div></Tabs>);
Concentric
Manage your account settings.
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";const PANELS = [{ value: "account", copy: "Manage your account settings." },{ value: "password", copy: "Change your password here." },{ value: "settings", copy: "Configure your preferences." },] as const;const LABELS: Record<string, string> = {account: "Account",password: "Password",settings: "Settings",};export default function TabsConcentric() {return (<Tabs defaultValue="account" concentric size="lg" className="w-fit max-w-full"><TabsList>{PANELS.map((panel) => (<TabsTrigger key={panel.value} value={panel.value}>{LABELS[panel.value]}</TabsTrigger>))}</TabsList><div className="relative min-h-[24px]">{PANELS.map((panel) => (<TabsContentkey={panel.value}value={panel.value}className="absolute inset-x-0 top-0"><p className="text-caption text-muted-foreground">{panel.copy}</p></TabsContent>))}</div></Tabs>);}
Override Styling
The list carries the only height and the only type size in the strip. Triggers
stretch to the first and inherit the second, and the indicator insets to the
list's own padding, so className="h-11 text-sm" on TabsList moves the whole
control the way a size step does — size is a preset for those same values,
not a separate axis.
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";export default function TabsOverrideStyling() {return (// `data-radius` scopes the shape system to this subtree. The list takes// the container radius and the triggers the element radius, so the corners// stay nested without either being written out here.<div data-radius="pill" className="w-full max-w-[380px]"><Tabs defaultValue="monthly" concentric size="lg">{/* The list's height and type size are the only ones in the strip —the triggers stretch to the first and inherit the second. */}<TabsList className="h-11 w-full p-1 text-sm"><TabsTrigger value="monthly" className="flex-1">Monthly</TabsTrigger><TabsTrigger value="yearly" className="flex-1">Yearly</TabsTrigger></TabsList><TabsContent value="monthly" className="text-muted-foreground text-sm">$12 per user, billed monthly.</TabsContent><TabsContent value="yearly" className="text-muted-foreground text-sm">$9 per user, billed annually. Two months free.</TabsContent></Tabs></div>);}
Radius rides the shape system rather than a utility: a data-radius scope
gives the list the container radius and the triggers the element radius, which
differ by exactly the list's 4px padding, so concentric corners stay nested
without any arithmetic at the call site.
Parts you hold no reference to are reachable by data-slot:
API Reference
Tabs
TabsList
TabsTrigger
TabsContent
A panel mounts the first time its tab becomes active, then stays
mounted — hidden, not unmounted, when another tab is selected — so
content backed by an API doesn't refetch and flash empty on every
revisit. forceMount mounts every panel up front instead of waiting
for a first visit.
TabsFromArray
The same tabs, driven by an array of TabItem objects instead of
JSX — for a list that comes from a config or an API rather than
being written out by hand.