Tabs

Organize content into labeled panels; one panel is visible at a time.

Overview

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
└── TabsContent

Examples

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.mutedfg.default). The selected tab keeps its fill on hover. Horizontal only.

Overview panel.
<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.

Overview panel.
<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)

PropTypeDefaultDescription
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.
defaultValuestringInitial open tab (value).
valuestringControlled selected tab.
onValueChange(value: string) => voidControlled 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 size prop ("sm" / "default" / "lg"). Not implemented yet — use className overrides 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

PropTypeDescription
valuestringRequired. Matches a trigger to its panel.

TabsContentSwap

PropTypeDescription
childrenReact.ReactNodeContent 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.

On this page