Overlays & navigation
Tabs
Tabs switch between views of the same thing (a client's overview, invoices and payments) without leaving the page. Choose the underline look for page sections, pills for filters inside a card, and segmented for a compact two-to-four way switch. Tabs take icons, counts and a disabled state, and follow the WAI-ARIA tabs pattern with automatic activation.
Fernhill Studio
- Outstanding
- €6,840.00
- Paid this year
- €48,210.00
- Average days to pay
- 18
Latest invoices
- INV-2041
- €4,200.00
- INV-2033
- €2,640.00
- INV-2019
- €1,980.00
Latest payments
- Mar 28 · Bank transfer
- €1,980.00
- Mar 02 · Card
- €3,100.00
Activity
- Ada Moreno opened INV-2041
- 2h
- Reminder sent
- 1d
const clientTabs: TabItem[] = [ { id: "overview", label: "Overview", icon: <LayoutGrid strokeWidth={1.75} />, content: <Panel title="Fernhill Studio" rows={[["Outstanding", "€6,840.00"], ["Paid this year", "€48,210.00"], ["Average days to pay", "18"]]} />, }, { id: "invoices", label: "Invoices", icon: <FileText strokeWidth={1.75} />, count: 12, content: <Panel title="Latest invoices" rows={[["INV-2041", "€4,200.00"], ["INV-2033", "€2,640.00"], ["INV-2019", "€1,980.00"]]} />, }, { id: "payments", label: "Payments", icon: <CreditCard strokeWidth={1.75} />, count: 8, content: <Panel title="Latest payments" rows={[["Mar 28 · Bank transfer", "€1,980.00"], ["Mar 02 · Card", "€3,100.00"]]} />, }, { id: "activity", label: "Activity", icon: <Activity strokeWidth={1.75} />, content: <Panel title="Activity" rows={[["Ada Moreno opened INV-2041", "2h"], ["Reminder sent", "1d"]]} />, }, { id: "settings", label: "Settings", icon: <Settings strokeWidth={1.75} />, disabled: true, content: null },]; <Tabs label="Client sections" items={clientTabs} className="mx-auto w-full max-w-xl" />Usage
import { Tabs } from "@/components/ui/tabs"; <Tabs label="Client sections" items={[ { id: "overview", label: "Overview", content: <Overview /> }, { id: "invoices", label: "Invoices", count: 12, content: <Invoices /> }, ]}/>Examples
Icons and counts
Counts use tabular figures so the labels don't shift. Disabled tabs are skipped by the arrow keys.
Fernhill Studio
- Outstanding
- €6,840.00
- Paid this year
- €48,210.00
- Average days to pay
- 18
Latest invoices
- INV-2041
- €4,200.00
- INV-2033
- €2,640.00
- INV-2019
- €1,980.00
Latest payments
- Mar 28 · Bank transfer
- €1,980.00
- Mar 02 · Card
- €3,100.00
Activity
- Ada Moreno opened INV-2041
- 2h
- Reminder sent
- 1d
const clientTabs: TabItem[] = [ { id: "overview", label: "Overview", icon: <LayoutGrid strokeWidth={1.75} />, content: <Panel title="Fernhill Studio" rows={[["Outstanding", "€6,840.00"], ["Paid this year", "€48,210.00"], ["Average days to pay", "18"]]} />, }, { id: "invoices", label: "Invoices", icon: <FileText strokeWidth={1.75} />, count: 12, content: <Panel title="Latest invoices" rows={[["INV-2041", "€4,200.00"], ["INV-2033", "€2,640.00"], ["INV-2019", "€1,980.00"]]} />, }, { id: "payments", label: "Payments", icon: <CreditCard strokeWidth={1.75} />, count: 8, content: <Panel title="Latest payments" rows={[["Mar 28 · Bank transfer", "€1,980.00"], ["Mar 02 · Card", "€3,100.00"]]} />, }, { id: "activity", label: "Activity", icon: <Activity strokeWidth={1.75} />, content: <Panel title="Activity" rows={[["Ada Moreno opened INV-2041", "2h"], ["Reminder sent", "1d"]]} />, }, { id: "settings", label: "Settings", icon: <Settings strokeWidth={1.75} />, disabled: true, content: null },]; <Tabs label="Client sections" items={clientTabs} className="mx-auto w-full max-w-xl" />Variants
underline (default), pills and segmented. All three share the same keyboard behaviour and markup.
128 invoices across every status.
9 invoices waiting for payment.
3 invoices past their due date.
Everything that has been settled.
128 invoices across every status.
9 invoices waiting for payment.
3 invoices past their due date.
Everything that has been settled.
128 invoices across every status.
9 invoices waiting for payment.
3 invoices past their due date.
Everything that has been settled.
<div className="mx-auto flex w-full max-w-xl flex-col gap-8"> {(["underline", "pills", "segmented"] as TabsVariant[]).map((v) => ( <div key={v} className="flex flex-col gap-2"> <span className="text-eyebrow">{v}</span> <Tabs label={`Invoice status (${v})`} variant={v} items={simple} /> </div> ))}</div>Full width
fitted stretches the list and shares the width equally, which suits segmented switches in narrow cards and on phones.
€24 per seat, billed every month.
€19 per seat, billed once a year. Save 20%.
<div className="mx-auto w-full max-w-xs"> <Tabs label="Billing period" variant="segmented" fitted items={[ { id: "monthly", label: "Monthly", content: <p className="m-0 text-center text-[13px] text-muted">€24 per seat, billed every month.</p> }, { id: "yearly", label: "Yearly", content: <p className="m-0 text-center text-[13px] text-muted">€19 per seat, billed once a year. Save 20%.</p> }, ]} /></div>Props
Tabs
itemsrequiredTabItem[]default: —The tabs and their panels, in order.variant"underline" | "pills" | "segmented"default: "underline"Visual style.defaultIdstringdefault: —Tab selected on first render (uncontrolled). Defaults to the first enabled tab.valuestringdefault: —Selected tab id (controlled). Pair with onValueChange.onValueChange(id: string) => voiddefault: —Called when the selection changes.fittedbooleandefault: falseStretch the tab list to the full width.labelstringdefault: —Accessible name of the tab list.classNamestringdefault: —Classes for the root element.
TabItem
idrequiredstringdefault: —Unique id; also written to data-tab and data-panel.labelrequiredstringdefault: —Tab text.contentrequiredReactNodedefault: —The panel shown when the tab is selected.iconReactNodedefault: —Lucide icon before the label.countnumberdefault: —Small count after the label.disabledbooleandefault: —Not selectable; skipped by the keyboard.
Accessibility
- Uses role tablist, tab and tabpanel. Each tab has aria-selected and aria-controls; each panel is labelled by its tab.
- Only the selected tab is in the Tab order. ArrowLeft and ArrowRight move between tabs and select them, Home and End jump to the first and last.
- Panels are focusable (tabindex=0) so keyboard users can reach content that has no focusable elements.
- Give the list a name with label when a page has more than one set of tabs.