Tabs
Switch between sibling views inside the same page context. Radix Tabs primitive with TWO Yuno-styled variants sharing the same API: `underline` (the Dashboard default — 2px primary underline on the active tab, muted default text, accent-tinted hover pill) and `pill` (the shadcn segmented control — bg-muted track with a white active surface). Same Root / List / Trigger / Content compound for both; the visual variant is set once on TabsList and read via Context by each Trigger.
Interactive playground
Toggle the props in the panel on the right and see the Tabs re-render in real time. The code block below reflects the exact JSX with the values you picked, ready to copy into your prototype.
Overview panel content.
<Tabs defaultValue="overview">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="rules">Rules</TabsTrigger>
<TabsTrigger value="lists">Lists</TabsTrigger>
</TabsList>
<TabsContent value="overview">…</TabsContent>
<TabsContent value="rules">…</TabsContent>
<TabsContent value="lists">…</TabsContent>
</Tabs>Anatomy
Four compound parts shared by both variants. Root manages state; List is the row of triggers; each Trigger is a tab button; Content is the panel that shows when its trigger is active.
Panel content lands here.
- 1Tabs (Root)
The controlled/uncontrolled state container. defaultValue = uncontrolled initial tab. value + onValueChange = controlled. orientation='vertical' is supported but rarely used in the Dashboard.
- 2TabsList
The row of triggers. Sets the visual variant via a variant prop (default 'underline'): 'underline' → inline-flex items-center gap-2 (no chrome); 'pill' → h-9 rounded-md bg-muted p-1 (segmented control). The variant flows to every Trigger inside via Context.
- 3TabsTrigger
One tab. In underline: outer py-1.5 with a border-b-2 transparent (turns primary when active) + inner span with px-2.5 py-2 rounded-md text-sm text-muted-foreground that picks up bg-accent + text-accent-foreground on hover. In pill: standard shadcn — px-3 py-1 rounded-sm with bg-background + shadow-xs on active.
- 4TabsContent
The panel. mt-4 in the kit (bumped from shadcn's mt-2 to give the primary underline breathing room). Focus-visible ring for keyboard users.
Variants
Two visual variants over the same Radix state. Pass variant on TabsList; every Trigger inside inherits it via Context — no prop drilling.
Panel content for Overview.
<Tabs defaultValue="overview">
<TabsList variant="underline">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="rules">Rules</TabsTrigger>
<TabsTrigger value="lists">Lists</TabsTrigger>
</TabsList>
<TabsContent value="overview">…</TabsContent>
</Tabs>Rendered as a list.
<Tabs defaultValue="list">
<TabsList variant="pill">
<TabsTrigger value="list">List</TabsTrigger>
<TabsTrigger value="grid">Grid</TabsTrigger>
</TabsList>
<TabsContent value="list">…</TabsContent>
</Tabs>States
Underline — default: text-muted-foreground, no chrome. Hover (inactive): bg-accent + text-accent-foreground on the inner text pill; the underline stays transparent. Active: text-foreground + 2px border-primary underline. Focus-visible: keyboard ring on the outer trigger. Disabled: opacity-50 + pointer-events-none. Pill — default: text-muted-foreground; active: bg-background + text-foreground + shadow-xs (white pill lifted over the muted track).
Recipes
Ready-to-copy compositions covering the most common Yuno usages of this atom.
Common in Dashboard nav tabs: a tab label + a small BadgeNumber counter. The counter picks up bg-primary so it reads as a soft signal, not a warning. Wrap the count in <BadgeNumber> or a compact inline span.
<Tabs defaultValue="alerts">
<TabsList variant="underline">
<TabsTrigger value="alerts">
Alerts <BadgeNumber>8</BadgeNumber>
</TabsTrigger>
<TabsTrigger value="rules">
Rules <BadgeNumber>12</BadgeNumber>
</TabsTrigger>
<TabsTrigger value="lists">
Lists <BadgeNumber>3</BadgeNumber>
</TabsTrigger>
</TabsList>
</Tabs>For sharable pages (transactions, rules), persist the active tab in the URL via query string. Use value + onValueChange, sync with a router param. The kit example uses local state; wire it to your router in production.
Active tab (state): overview
Overview content.
const [tab, setTab] = React.useState<string>("overview");
// wire to router.push({ query: { tab } }) in prod
<Tabs value={tab} onValueChange={setTab}>
<TabsList variant="underline">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="details">Details</TabsTrigger>
</TabsList>
<TabsContent value="overview">…</TabsContent>
<TabsContent value="details">…</TabsContent>
</Tabs>Use the pill variant for compact inline switches — a small view-mode toggle inside a Card header (list vs grid, chart period). It's tighter than the underline and lives comfortably in constrained surfaces.
Volume by day
<div className="flex items-center justify-between">
<h3 className="text-base font-semibold">Volume by day</h3>
<Tabs defaultValue="7d">
<TabsList variant="pill">
<TabsTrigger value="7d">7d</TabsTrigger>
<TabsTrigger value="30d">30d</TabsTrigger>
<TabsTrigger value="90d">90d</TabsTrigger>
</TabsList>
</Tabs>
</div>The most common Dashboard shape: tabs above, a Card wrapping the panel content below. mt-4 default gap keeps the primary underline visible above the Card border.
<Tabs defaultValue="overview">
<TabsList variant="underline">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="details">Details</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<div className="rounded-lg border bg-card p-6">…</div>
</TabsContent>
</Tabs>Import
Single compound import from @/components/ui/tabs.
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs";Props
Radix Tabs props on Root/List/Trigger/Content. The Yuno-specific prop is TabsList.variant:
| Prop | Type | Default | Description |
|---|---|---|---|
| Tabs.defaultValue | string | — | Uncontrolled initial active tab (matches a Trigger's value). Use for first render — Radix keeps state internally after that. |
| Tabs.value / onValueChange | string / (value: string) => void | — | Controlled pair. Reach for these when the tab state needs to live in the URL, in Zustand, or in the parent form state. |
| Tabs.orientation | "horizontal" | "vertical" | "horizontal" | Layout axis. Vertical is supported but rare in the Dashboard — the sidebar already covers that pattern. |
| TabsList.variant | "underline" | "pill" | "underline" | Yuno-specific. Sets the visual treatment for the whole compound via Context — every Trigger inside picks it up without prop drilling. 'underline' = page-level Yuno tabs (2px primary underline). 'pill' = compact segmented control (shadcn style). |
| TabsTrigger.value | string | — | Required. Matches Tabs.defaultValue / value and pairs the Trigger with its Content sibling. |
| TabsTrigger.disabled | boolean | false | Disables the tab (opacity-50, pointer-events-none). Radix skips it in keyboard nav. |
| TabsContent.value | string | — | Required. Matches the Trigger's value. Radix renders exactly one Content at a time. |
| TabsContent.forceMount | boolean | false | Radix escape hatch — force-render inactive content (useful for animation libraries that need the DOM present). Rarely needed in the Dashboard. |
When to use
- 2–5 sibling views under the same page context (Overview / Rules / Lists).
- Filter across categories inside a page.
- View-mode toggles inside a Card (pill variant only).
When not to use
- Navigating to a different page or scope — use Sidebar navigation.
- More than ~7 tabs — the row starts wrapping or scrolling and IA gets muddy.
- Read-only categorization — Tabs imply the user can switch and see distinct content.
Usage
- Keep tab labels short (1–2 words).
- Default to the most-used tab so first render lands on the common case.
- Persist the active tab in the URL when it's a shareable state.
- Use the underline variant for page-level tabs; the pill variant only for inline switches inside a component.
- Pair labels with a BadgeNumber counter when the count is a first-class signal (Insights, Alerts).
- Don't hide critical primary actions behind a non-default tab.
- Don't mix Tabs with a Sidebar for the same navigation level.
- Don't use the pill variant for page-level tabs — the muted track was designed for compact segmented switches, not for the top of a page.
- Don't override the underline color — bg-primary is the Yuno tab-active signal.
- Don't stack two Tabs rows in the same view.
Related
Cross-links to atoms and patterns you may reach for next.
- Sidebar (organism)For navigating between routes / scopes — Tabs are strictly for sibling views within one route.
- Toggle groupSimilar visual for exclusive/multi-select option groups (not view switching).
- Radio groupFor single-select choices inside forms — not for switching content areas.
- BadgeBadgeNumber composes cleanly next to the tab label for counters.