Tabs
Organize content into labeled panels; one panel is visible at a time.
View your key metrics and recent project activity. Track progress across all your active projects.
You have 12 active projects and 3 pending tasks.
<Tabs defaultValue="overview" className="w-full max-w-md gap-4"> <TabsList> <TabsTrigger value="overview">Overview</TabsTrigger> <TabsTrigger value="analytics">Analytics</TabsTrigger> <TabsTrigger value="reports">Reports</TabsTrigger> <TabsTrigger value="settings">Settings</TabsTrigger> </TabsList> <TabsContent value="overview"> <Card> <CardHeader> <CardTitle>Overview</CardTitle> <CardDescription> View your key metrics and recent project activity. Track progress across all your active projects. </CardDescription> </CardHeader> <CardContent className="text-sm text-fg-muted"> You have 12 active projects and 3 pending tasks. </CardContent> </Card> </TabsContent> <TabsContent value="analytics"> <Card> <CardHeader> <CardTitle>Analytics</CardTitle> <CardDescription> Track performance and user engagement metrics. Monitor trends and identify growth opportunities. </CardDescription> </CardHeader> <CardContent className="text-sm text-fg-muted"> Page views are up 25% compared to last month. </CardContent> </Card> </TabsContent> <TabsContent value="reports"> <Card> <CardHeader> <CardTitle>Reports</CardTitle> <CardDescription> Generate and download your detailed reports. Export data in multiple formats for analysis. </CardDescription> </CardHeader> <CardContent className="text-sm text-fg-muted"> You have 5 reports ready and available to export. </CardContent> </Card> </TabsContent> <TabsContent value="settings"> <Card> <CardHeader> <CardTitle>Settings</CardTitle> <CardDescription> Manage your account preferences and options. Customize your experience to fit your needs. </CardDescription> </CardHeader> <CardContent className="text-sm text-fg-muted"> Configure notifications, security, and themes. </CardContent> </Card> </TabsContent></Tabs>Usage
import { Tabs, TabsBar, TabsContent, TabsContentSwap, TabsList, TabsTrigger } from "@reva/ui";Use defaultValue (uncontrolled) or value + onValueChange (controlled). Each TabsTrigger and TabsContent shares the same value string.
For tab panes that should animate on switch, render a single TabsContentSwap inside Tabs instead of multiple TabsContent siblings — see Animated content swap.
Tabs render as the underline rail by default; pass variant="ghost" for a compact ghost-button bar — a good fit for dense in-canvas section nav or quick filters. See Ghost variant.
Composition
Use the following composition to build tabs:
Tabs
├── TabsBar (optional — allows Select/Button siblings on the rail)
│ └── TabsList
│ ├── TabsTrigger
│ └── TabsTrigger
├── TabsList
│ ├── TabsTrigger
│ └── TabsTrigger
├── TabsContent
└── TabsContentExamples
Ghost variant
Set variant="ghost" for a compact ghost-button bar instead of the underline rail. There's no rail: the list is an inline-flex row with a 2px gap, and each tab is a ghost pill — transparent at rest, a faint hover fill while inactive (bg-faint), and a surface.muted fill when selected (ink fg.muted → fg.default). The selected tab keeps its fill on hover. Horizontal only.
<Tabs variant="ghost" defaultValue="overview" className="w-full"> <TabsList> <TabsTrigger value="overview">Overview</TabsTrigger> <TabsTrigger value="cash-flow">Cash flow</TabsTrigger> <TabsTrigger value="net-worth">Net worth</TabsTrigger> <TabsTrigger value="events">Events</TabsTrigger> </TabsList> <TabsContent value="overview"> <div className="text-sm">Overview panel.</div> </TabsContent> <TabsContent value="cash-flow"> <div className="text-sm">Cash flow panel.</div> </TabsContent> <TabsContent value="net-worth"> <div className="text-sm">Net worth panel.</div> </TabsContent> <TabsContent value="events"> <div className="text-sm">Events panel.</div> </TabsContent></Tabs>Animated content swap
Use TabsContentSwap for a single animated pane that cross-slides (with blur) when the active tab changes. Direction is derived automatically from the authored order of TabsTrigger children, and the motion respects prefers-reduced-motion.
Unlike TabsContent, you do not pass a value: render whatever content matches the current active tab (typically by reading your own controlled state, or a router), and the component handles the swap.
Overview
Track your project progress across all active workstreams and milestones.
12
Active
84
Complete
94%
Velocity
"use client";import {Card,CardContent,Heading,HStack,Stack,Tabs,TabsContentSwap,TabsList,TabsTrigger,Text,VStack,} from "@reva/ui";import { Gear, Pulse, SquaresFour } from "@phosphor-icons/react/ssr";import { useState } from "react";const PANELS = {overview: { icon: SquaresFour, iconClass: "bg-purple-500", title: "Overview", description: "Track your project progress across all active workstreams and milestones.", stats: [ { value: "12", label: "Active" }, { value: "84", label: "Complete" }, { value: "94%", label: "Velocity" }, ],},activity: { icon: Pulse, iconClass: "bg-red-500", title: "Activity", description: "Recent changes, commits, and team updates from the last 7 days.", stats: [ { value: "47", label: "Commits" }, { value: "23", label: "Reviews" }, { value: "18", label: "Merged" }, ],},settings: { icon: Gear, iconClass: "bg-blue-500", title: "Settings", description: "Configure notifications, access controls, and integration preferences.", stats: [ { value: "8", label: "Members" }, { value: "3", label: "Roles" }, { value: "5", label: "Hooks" }, ],},};export function TabsContentSwapDemo() {const [tab, setTab] = useState("overview");const panel = PANELS[tab];const Icon = panel.icon;return ( <Tabs value={tab} onValueChange={setTab} className="w-full max-w-xl gap-4"> <TabsList className="w-full"> <TabsTrigger value="overview">Overview</TabsTrigger> <TabsTrigger value="activity">Activity</TabsTrigger> <TabsTrigger value="settings">Settings</TabsTrigger> </TabsList> <TabsContentSwap> <Card> <CardContent> <Stack gap={4}> <HStack gap={3} align="center"> <span className={`flex size-10 items-center justify-center rounded-lg text-white ${panel.iconClass}`}> <Icon aria-hidden className="size-5" /> </span> <Heading as="h3" size="2xl" weight="semibold"> {panel.title} </Heading> </HStack> <Text color="muted-foreground">{panel.description}</Text> <HStack className="rounded-lg border border-border-default bg-muted/40 p-4" gap={0}> {panel.stats.map((stat) => ( <VStack key={stat.label} gap={1} align="center" className="flex-1"> <Heading as="p" size="2xl" weight="semibold"> {stat.value} </Heading> <Text textStyle="caption1" color="muted-foreground" weight="medium"> {stat.label} </Text> </VStack> ))} </HStack> </Stack> </CardContent> </Card> </TabsContentSwap> </Tabs>);}Use at most one TabsContentSwap per Tabs root. Do not mix it with sibling TabsContent panels.
With a Select and actions
Wrap TabsList in TabsBar to render arbitrary siblings — a Select before the tabs, action buttons after — while keeping the underline border spanning the full row. TabsBar is horizontal-only; when present, the nested TabsList automatically drops its own border and height.
<Tabs defaultValue="overview" className="w-full"> <TabsBar> <Select defaultValue="household" size="sm"> <SelectTrigger variant="secondary" className="w-[160px]"> <SelectValue /> </SelectTrigger> <SelectContent> <SelectItem value="household">Household</SelectItem> <SelectItem value="individual">Individual</SelectItem> </SelectContent> </Select> <TabsList> <TabsTrigger value="overview">Overview</TabsTrigger> <TabsTrigger value="details">Details</TabsTrigger> <TabsTrigger value="wealth">Wealth</TabsTrigger> <TabsTrigger value="activity">Activity</TabsTrigger> </TabsList> <HStack className="ml-auto" gap={2}> <Button variant="ghost" size="sm">Filter</Button> <Button size="sm">New</Button> </HStack> </TabsBar> <TabsContent value="overview"> <div className="text-sm">Overview panel.</div> </TabsContent> <TabsContent value="details"> <div className="text-sm">Details panel.</div> </TabsContent> <TabsContent value="wealth"> <div className="text-sm">Wealth panel.</div> </TabsContent> <TabsContent value="activity"> <div className="text-sm">Activity panel.</div> </TabsContent></Tabs>Props
Radix Tabs forwards props to the root; below are the main surface APIs.
Tabs (root)
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "line" | "ghost" | "line" | Visual style: the underline rail (line) or a compact ghost-button bar (ghost). |
orientation | "horizontal" | "vertical" | "horizontal" | Stack list and content. |
defaultValue | string | — | Initial open tab (value). |
value | string | — | Controlled selected tab. |
onValueChange | (value: string) => void | — | Controlled change handler. |
TabsBar
Optional row wrapper. Renders a horizontal flex row (h-12, gap-4) that owns the border-b so TabsList siblings (Select, Button, etc.) sit on the same underline. Forwards all div props.
TabsList
Forwards all Radix Tabs.List props. The list is full-width with a bottom border; the active tab carries the amber underline indicator and tabs are content-sized. The bar height is 48px.
Planned: a
sizeprop ("sm"/"default"/"lg"). Not implemented yet — useclassNameoverrides for a different height in the meantime.
Alignment. Tabs pack from the left by default (justify-start). For the edge cases where you need a different alignment, use Tailwind utilities via className:
<TabsList className="justify-center">…</TabsList>
<TabsList className="justify-end">…</TabsList>TabsTrigger / TabsContent
| Prop | Type | Description |
|---|---|---|
value | string | Required. Matches a trigger to its panel. |
TabsContentSwap
| Prop | Type | Description |
|---|---|---|
children | React.ReactNode | Content for the currently active tab. The consumer decides what to render based on their tab state; the component keys the animated pane by the active Tabs value. |
| … | React.HTMLAttributes<"div"> | Forwarded to the inner motion pane. |