Browse docs
Browse docs
Tabs organizes related content into a single panel, switched by a strip of
triggers. It is a custom, clean-room component — it does not wrap
@mui/material's Tabs. The selection state and keyboard model come from a
headless useTabs engine implementing the
WAI-ARIA APG tabs pattern.
Tabs is standalone navigation — no form bridge, no RBAC. The whole tab set is
described declaratively through a single items array.
import { Tabs } from '@dashforge/ui';
const items = [
{ value: 'overview', label: 'Overview', content: <p>Overview panel</p> },
{ value: 'activity', label: 'Activity', content: <p>Activity panel</p> },
{ value: 'settings', label: 'Settings', content: <p>Settings panel</p> },
];
<Tabs items={items} onValueChange={(value) => console.log(value)} />A basic uncontrolled tab set — the first item is active by default, the engine manages the selection from there.
A high-level summary of the record.
const items = [
{ value: 'overview', label: 'Overview', content: <Typography variant="body2">A high-level summary of the record.</Typography> },
{ value: 'activity', label: 'Activity', content: <Typography variant="body2">The recent activity feed for this record.</Typography> },
{ value: 'settings', label: 'Settings', content: <Typography variant="body2">Settings form fields go here.</Typography> },
];
<Tabs items={items} />variant="pill" swaps the underline indicator for a filled pill. A per-item
disabled flag greys a trigger out and skips it during keyboard navigation.
A high-level summary of the record.
const items = [
{ value: 'overview', label: 'Overview', content: <Typography variant="body2">A high-level summary of the record.</Typography> },
{ value: 'activity', label: 'Activity', content: <Typography variant="body2">The recent activity feed for this record.</Typography> },
{ value: 'settings', label: 'Settings', content: <Typography variant="body2">Settings form fields go here.</Typography> },
{ value: 'archived', label: 'Archived', content: <Typography variant="body2">Archived items.</Typography>, disabled: true },
];
<Tabs items={items} variant="pill" />orientation="vertical" stacks the triggers in a column beside the panel.
Arrow-key navigation follows the axis — ArrowUp / ArrowDown instead of
ArrowLeft / ArrowRight.
General account settings.
const items = [
{ value: 'general', label: 'General', content: <Typography variant="body2">General account settings.</Typography> },
{ value: 'security', label: 'Security', content: <Typography variant="body2">Password and two-factor authentication.</Typography> },
{ value: 'billing', label: 'Billing', content: <Typography variant="body2">Plan, invoices, and payment method.</Typography> },
];
<Tabs items={items} orientation="vertical" />Pass value + onValueChange to own the selection in your own state. With
keepMounted, every panel stays mounted (inactive ones hidden via the hidden
attribute) so panel state survives a tab switch.
A high-level summary of the record.
The recent activity feed for this record.
Settings form fields go here.
Active tab: activity
const [active, setActive] = useState('activity');
<Tabs items={items} value={active} onValueChange={setActive} keepMounted />| Field | Type | Default | Description |
|---|---|---|---|
value | string | — | Canonical id for the tab — the selection key. |
label | ReactNode | — | Trigger button content. |
content | ReactNode | — | Panel body rendered when this tab is active. |
disabled | boolean | false | Greys the trigger out; keyboard nav skips it. |
| Prop | Type | Default | Description |
|---|---|---|---|
items | TabItem[] | — | The tab set. |
value | string | — | Active tab value (controlled). |
defaultValue | string | first item | Initial active value (uncontrolled). |
onValueChange | (value: string) => void | — | Fired when the active tab changes. |
variant | 'underline' | 'pill' | 'underline' | Visual style of the trigger strip. |
orientation | 'horizontal' | 'vertical' | 'horizontal' | Trigger-strip axis. |
keepMounted | boolean | false | Keep inactive panels mounted (hidden). |
testId | string | — | Test id applied to the root element. |
role="tablist" with
role="tab" triggers and role="tabpanel" panels, wired together by
aria-controls / aria-labelledby.orientation), Home / End jump to the
first / last tab, and focus wraps at the edges.Tabs has no name, no validation, no RBAC. It
does not register with DashForm.keepMounted when a panel holds state (a
half-filled form, a scroll position) that must survive a switch.Tabs. The @dashforge/tw Tabs mirrors this
prop surface — only the sx / slotProps override hooks differ between the
two skins.