Components

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.

Updated Jul 17, 2026 by Leonardo Posada

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.

  1. 1
    Tabs (Root)

    The controlled/uncontrolled state container. defaultValue = uncontrolled initial tab. value + onValueChange = controlled. orientation='vertical' is supported but rarely used in the Dashboard.

  2. 2
    TabsList

    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.

  3. 3
    TabsTrigger

    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.

  4. 4
    TabsContent

    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.

Underline (Yuno default)

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>
Pill (shadcn segmented)

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

Underline for pages, pill for inline switches
Reach for the underline variant (default) whenever the tabs represent primary sibling views on a page — the same level as a heading. Reach for the pill variant when the tabs are a compact inline switch inside a component (view mode, chart period, panel filter). Never use pill for page-level navigation — the muted track fights the surrounding chrome.

Recipes

Ready-to-copy compositions covering the most common Yuno usages of this atom.

Underline with badge counter

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>
Controlled (URL-persisted)

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>
Pill (segmented view mode)

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>
Card + content panel

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.

Panel wrapped in a Card. The 24px mt 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.

One import. TabsList reads variant on its own; every Trigger inside inherits it via Context.
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:

PropTypeDefaultDescription
Tabs.defaultValuestringUncontrolled initial active tab (matches a Trigger's value). Use for first render — Radix keeps state internally after that.
Tabs.value / onValueChangestring / (value: string) => voidControlled 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.valuestringRequired. Matches Tabs.defaultValue / value and pairs the Trigger with its Content sibling.
TabsTrigger.disabledbooleanfalseDisables the tab (opacity-50, pointer-events-none). Radix skips it in keyboard nav.
TabsContent.valuestringRequired. Matches the Trigger's value. Radix renders exactly one Content at a time.
TabsContent.forceMountbooleanfalseRadix 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

Do
  • 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
  • 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.