Tabs
A strip of triggers where one is current, with an animated indicator. The indicator uses the primitive's CSS variables (`--active-tab-*`), not React measurement — it tracks on resize and under RTL automatically.
npx shadcn@latest add @fragiola/tabsAccount settings — make changes to your profile.
Content A.
General settings.
Show code
"use client";
import type { ReactNode } from "react";
import { Tabs } from "#/ui/tabs";
// The floor is palette-surface. The tabs — list, triggers and panels — read
// roles from the floor. The active tab uses contrast, inactive tabs use
// accent, panel text uses accent/85. Shows: horizontal with indicator,
// disabled tab, and vertical.
export default function TabsDemo() {
return (
<div className="palette-surface flex flex-col gap-6 rounded-lg border border-palette-line bg-palette-base p-6">
<Row label="horizontal">
<Tabs.Root defaultValue="account">
<Tabs.List>
<Tabs.Tab value="account">Account</Tabs.Tab>
<Tabs.Tab value="password">Password</Tabs.Tab>
<Tabs.Tab value="team">Team</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="account">
<p className="text-sm text-palette-accent/85">
Account settings — make changes to your profile.
</p>
</Tabs.Panel>
<Tabs.Panel value="password">
<p className="text-sm text-palette-accent/85">
Password settings — update your password.
</p>
</Tabs.Panel>
<Tabs.Panel value="team">
<p className="text-sm text-palette-accent/85">
Team settings — manage your team members.
</p>
</Tabs.Panel>
</Tabs.Root>
</Row>
<Row label="disabled tab">
<Tabs.Root defaultValue="a">
<Tabs.List>
<Tabs.Tab value="a">Tab A</Tabs.Tab>
<Tabs.Tab value="b">Tab B</Tabs.Tab>
<Tabs.Tab value="c" disabled>
Tab C (disabled)
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="a">
<p className="text-sm text-palette-accent/85">
Content A.
</p>
</Tabs.Panel>
<Tabs.Panel value="b">
<p className="text-sm text-palette-accent/85">
Content B.
</p>
</Tabs.Panel>
</Tabs.Root>
</Row>
<Row label="vertical">
<Tabs.Root defaultValue="general" orientation="vertical">
<Tabs.List>
<Tabs.Tab value="general">General</Tabs.Tab>
<Tabs.Tab value="billing">Billing</Tabs.Tab>
<Tabs.Tab value="notifications">Notifications</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="general">
<p className="text-sm text-palette-accent/85">
General settings.
</p>
</Tabs.Panel>
<Tabs.Panel value="billing">
<p className="text-sm text-palette-accent/85">
Billing settings.
</p>
</Tabs.Panel>
<Tabs.Panel value="notifications">
<p className="text-sm text-palette-accent/85">
Notification preferences.
</p>
</Tabs.Panel>
</Tabs.Root>
</Row>
</div>
);
}
function Row({ label, children }: { label: string; children: ReactNode }) {
return (
<div className="flex flex-col gap-2">
<span className="text-xs font-mono text-palette-accent/85">
{label}
</span>
{children}
</div>
);
}
No navigation family
Tabs, navigation-menu and pagination are all "a strip of triggers", but the overlap is thinner than it first looks. Tabs and navigation-menu share state normalisation (the highlighted custom variant) but differ in cursor, rounding and state-specific treatments. Pagination is Clickable in a <nav>. No family was created — the negative result is recorded in the port report.
Parts
| Part | Source |
|---|---|
Root | Base UI Tabs.Root |
List | layout + border-palette-line |
Tab | highlighted + palette-ring + layout |
Indicator | primitive CSS variables + bg-palette-base |
Panel | Base UI Tabs.Panel |
Collapsible
A single expand/collapse section. Composes the `disclosure` family (trigger, panel, content). An accordion is a group of collapsibles with coordination; a collapsible is one section on its own.
Navigation Menu
A strip of triggers where each opens a morphing popup with a viewport that resizes between items. The popup is `popup.content` — the same floating box dropdown-menu and popover use. The viewport morphs via the primitive's CSS variables, not a new popup member.