Tabs
Display one panel at a time, helping users switch between different views or subsections.
Overview
The Tabs component organises related content into separate panels that users can switch between, providing an efficient way to present multiple views or sections within the same interface area. Built on Base UI primitives, it offers full keyboard navigation and accessibility features.
Usage
import {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
} from "@tilt-legal/cubitt-components/primitives";<Tabs defaultValue="account">
<TabsList>
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
<TabsTrigger value="billing">Billing</TabsTrigger>
</TabsList>
<TabsContent value="account">Account content</TabsContent>
<TabsContent value="settings">Settings content</TabsContent>
<TabsContent value="billing">Billing content</TabsContent>
</Tabs>Using Cubitt's URL-state hooks, you can sync the tabs state with the URL by providing a paramName:
// The selected value will be synced with ?tab=account or ?tab=settings
<Tabs paramName="tab" defaultValue="account">
<TabsList variant="line">
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
<TabsTrigger value="billing">Billing</TabsTrigger>
</TabsList>
<TabsContent value="account">Account content</TabsContent>
<TabsContent value="settings">Settings content</TabsContent>
<TabsContent value="billing">Billing content</TabsContent>
</Tabs>
// Advanced options:
<Tabs
paramName="section"
paramClearOnDefault={true} // Remove param when value equals default
paramDebounce={300} // Debounce URL updates
onUrlValueChange={(value) => console.log('Active tab:', value)}
defaultValue="home"
>
{/* ... */}
</Tabs>TabsTrigger forwards Base UI's polymorphic render support. When the trigger renders as something other than a native <button> such as a router link, pass nativeButton={false} so keyboard and ARIA behavior stay correct.
import { Link } from "@tanstack/react-router";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@tilt-legal/cubitt-components/primitives";
<Tabs defaultValue="overview">
<TabsList>
<TabsTrigger
value="overview"
nativeButton={false}
render={<Link href="/overview" />}
>
Overview
</TabsTrigger>
<TabsTrigger value="usage">Usage</TabsTrigger>
</TabsList>
<TabsContent value="overview">Overview content</TabsContent>
<TabsContent value="usage">Usage content</TabsContent>
</Tabs>Examples
Default
import {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
} from "@tilt-legal/cubitt-components/primitives";
export default function Component() {
return (
<Tabs defaultValue="account" className="w-full max-w-md">
<TabsList>
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
<TabsTrigger value="billing">Billing</TabsTrigger>
</TabsList>
<TabsContent value="account">
<p className="text-sm text-muted-foreground">
Manage your account settings and preferences.
</p>
</TabsContent>
<TabsContent value="settings">
<p className="text-sm text-muted-foreground">
Configure your application settings.
</p>
</TabsContent>
<TabsContent value="billing">
<p className="text-sm text-muted-foreground">
View and manage your billing information.
</p>
</TabsContent>
</Tabs>
);
}Line
import {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
} from "@tilt-legal/cubitt-components/primitives";
export default function Component() {
return (
<Tabs defaultValue="account" className="w-full max-w-md">
<TabsList variant="line">
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
<TabsTrigger value="billing">Billing</TabsTrigger>
</TabsList>
<TabsContent value="account">
<p className="text-sm text-muted-foreground">
Manage your account settings and preferences.
</p>
</TabsContent>
<TabsContent value="settings">
<p className="text-sm text-muted-foreground">
Configure your application settings.
</p>
</TabsContent>
<TabsContent value="billing">
<p className="text-sm text-muted-foreground">
View and manage your billing information.
</p>
</TabsContent>
</Tabs>
);
}With Separator
Use TabsSeparator inside the default track when you need a visual split between groups of tabs. The separator fades out while the selected pill touches either side of it, then reappears once the indicator moves away.
import {
Tabs,
TabsContent,
TabsList,
TabsSeparator,
TabsTrigger,
} from "@tilt-legal/cubitt-components/primitives";
export default function Component() {
return (
<Tabs defaultValue="account" className="w-full max-w-md">
<TabsList>
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
<TabsSeparator />
<TabsTrigger value="billing">Billing</TabsTrigger>
</TabsList>
<TabsContent value="account">Account content</TabsContent>
<TabsContent value="settings">Settings content</TabsContent>
<TabsContent value="billing">Billing content</TabsContent>
</Tabs>
);
}Sizes
import { Tabs, TabsList, TabsTrigger } from "@tilt-legal/cubitt-components/primitives";
export default function Component() {
return (
<div className="flex flex-col items-center gap-12 py-6 w-full">
<Tabs defaultValue="account">
<TabsList size="sm" variant="line">
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
<TabsTrigger value="billing">Billing</TabsTrigger>
</TabsList>
</Tabs>
<Tabs defaultValue="account">
<TabsList size="md" variant="line">
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
<TabsTrigger value="billing">Billing</TabsTrigger>
</TabsList>
</Tabs>
<Tabs defaultValue="account">
<TabsList size="lg" variant="line">
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
<TabsTrigger value="billing">Billing</TabsTrigger>
</TabsList>
</Tabs>
</div>
);
}Disabled
import {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
} from "@tilt-legal/cubitt-components/primitives";
export default function Component() {
return (
<Tabs defaultValue="account" className="w-full max-w-md">
<TabsList variant="line">
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="settings" disabled>
Settings
</TabsTrigger>
<TabsTrigger value="billing">Billing</TabsTrigger>
</TabsList>
<TabsContent value="account">
<p className="text-sm text-muted-foreground">
Account settings are available.
</p>
</TabsContent>
<TabsContent value="billing">
<p className="text-sm text-muted-foreground">
Billing information is here.
</p>
</TabsContent>
</Tabs>
);
}Animated Height
Combine tabs with the AutoResize component for smooth height transitions between tab panels.
import {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
AutoResize,
Skeleton,
} from "@tilt-legal/cubitt-components/primitives";
export default function Component() {
return (
<Tabs defaultValue="account" className="w-full max-w-md">
<TabsList className="mb-4" size="sm" variant="line">
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
<TabsTrigger value="billing">Billing</TabsTrigger>
</TabsList>
<div className="bg-muted/50 text-card-foreground flex flex-col gap-4 rounded-lg border border-border/30 p-6">
<AutoResize>
<TabsContent value="account">
<div className="flex flex-col gap-6">
<div className="flex gap-4">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="flex-1 flex flex-col justify-center gap-2">
<Skeleton className="h-4 w-2/3" />
<Skeleton className="h-4 w-1/3" />
</div>
</div>
<div className="flex flex-col gap-2">
<Skeleton className="h-4" />
<Skeleton className="h-4 w-2/3" />
</div>
</div>
</TabsContent>
<TabsContent value="settings">
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-2">
<Skeleton className="h-8 w-1/3 mb-2" />
<Skeleton className="h-4" />
<Skeleton className="h-4 w-2/3" />
</div>
<div className="flex gap-4 justify-between">
<div className="flex-1 flex flex-col justify-center gap-2">
<Skeleton className="h-4 w-2/3" />
<Skeleton className="h-4 w-1/3" />
</div>
<Skeleton className="size-6" />
</div>
</div>
</TabsContent>
<TabsContent value="billing">
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-2">
<Skeleton className="h-8 w-2/3 mb-2" />
<Skeleton className="h-4" />
<Skeleton className="h-4 w-3/4" />
</div>
<div className="flex flex-col gap-2">
<Skeleton className="h-6 w-1/3" />
<div className="flex items-center justify-between gap-4">
<Skeleton className="h-4 w-2/3" />
<Skeleton className="h-6 w-10" />
</div>
</div>
</div>
</TabsContent>
</AutoResize>
</div>
</Tabs>
);
}URL State
This example syncs with the URL parameter "demo-tab". Try switching tabs and refreshing the page or sharing the URL.
import {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
AutoResize,
Skeleton,
} from "@tilt-legal/cubitt-components/primitives";
export default function Component() {
return (
<Tabs
paramName="demo-tab"
defaultValue="account"
className="w-full max-w-md"
>
<TabsList className="mb-4" size="sm" variant="line">
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
<TabsTrigger value="billing">Billing</TabsTrigger>
</TabsList>
<div className="bg-muted/50 text-card-foreground flex flex-col gap-4 rounded-lg border border-border/30 p-6">
<AutoResize>
<TabsContent value="account">
{/* Account content with varying heights */}
</TabsContent>
<TabsContent value="settings">
{/* Settings content with different height */}
</TabsContent>
<TabsContent value="billing">{/* Billing content */}</TabsContent>
</AutoResize>
</div>
</Tabs>
);
}API Reference
Tabs
The root component for creating tabbed interfaces with multiple variants and sizes.
| Prop | Type | Default | Description |
|---|---|---|---|
defaultValue | string | — | The default active tab when uncontrolled. |
value | string | — | The controlled active tab value. |
onValueChange | (value: string) => void | — | Callback fired when the active tab changes. |
disabled | boolean | false | Whether all tabs are disabled. |
className | string | — | Additional CSS classes for the tabs container. |
URL State Props
When provided with a paramName, the tabs will sync their state with URL parameters via TanStack Router search params.
| Prop | Type | Default | Description |
|---|---|---|---|
paramName | string | — | URL parameter name for syncing state with URL. Enables URL state management. |
onUrlValueChange | (value: string) => void | — | Callback when URL parameter value changes. |
paramClearOnDefault | boolean | true | Remove URL param when value equals default. |
paramDebounce | number | — | Debounce URL updates in milliseconds. |
paramThrottle | number | — | Throttle URL updates in milliseconds. |
TabsList
Container for tab triggers with variant and size controls.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "line" | "default" | The visual variant of the tabs list. default uses the segmented control styling. |
size | "sm" | "md" | "lg" | "md" | The size of the tabs. |
className | string | — | Additional CSS classes for the tabs list. |
TabsTrigger
Individual tab trigger that users click to switch panels. Supports Base UI's polymorphic render prop for custom elements such as router links.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | The value of the tab trigger (required). |
disabled | boolean | false | Whether the tab trigger is disabled. |
render | ReactElement | function | — | Render the trigger as a custom element or component while preserving tab state. |
nativeButton | boolean | true | Set to false when render outputs a non-button element like Link or a. |
className | string | — | Additional CSS classes for the tab trigger. |
TabsContent
The content panel for each tab.
| Prop | Type | Description |
|---|---|---|
value | string | The value of the tab content (required). |
className | string | Additional CSS classes for the tab content. |