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 — 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/tw';
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.
import { Tabs, Typography } from '@dashforge/tw';
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.
import { Tabs, Typography } from '@dashforge/tw';
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.
import { Tabs, Typography } from '@dashforge/tw';
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.
import { useState } from 'react';
import { Tabs, Stack, Typography } from '@dashforge/tw';
function ControlledTabs() {
const [active, setActive] = useState('activity');
return (
<Stack gap={2}>
<Tabs items={items} value={active} onValueChange={setActive} keepMounted />
<Typography variant="caption" sx="text-neutral-500">
Active tab: {active}
</Typography>
</Stack>
);
}Configure <Tabs> defaults application-wide.
import { patchTheme } from '@dashforge/tw-theme';
patchTheme({
components: {
Tabs: {
defaults: { variant: 'underline', orientation: 'horizontal' },
},
},
});Configurable axes (TabsVariantProps):
| Axis | Type | Notes |
|---|---|---|
variant | 'underline' | 'pill' | Visual treatment of the trigger strip. |
orientation | 'horizontal' | 'vertical' | Trigger strip axis (also drives arrow-key nav direction). |
Non-visual axes (items, value, defaultValue, onValueChange, keepMounted) are per-instance — data and state — not theme-configurable.
Precedence chain (lowest → highest):
defaultVariants from the internal tabsVariants recipe (variant: 'underline', orientation: 'horizontal').theme.components.Tabs.defaults (application-wide).<Tabs variant="pill" orientation="vertical" />) — wins over theme.sx — appended to the root and wins over conflicting classes via tailwind-merge.TypeScript: theme.components.Tabs.defaults autocompletes to the two axes above.
Reactivity: useComponentDefaults('Tabs') subscribes to the theme store — patchTheme re-renders every mounted instance inheriting the changed axis.
| 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[] | — | Tab items array. |
defaultValue | string | — | Default active value (uncontrolled). Defaults to first item's value. |
keepMounted | boolean | — | Keep inactive panels mounted (hidden via the hidden attribute). Default false — only the active panel is in the DOM. |
onValueChange | (value: string) => void | — | Fires when the active tab changes. |
orientation | 'horizontal' | 'vertical' | 'horizontal' | Trigger strip axis. vertical orientation also switches arrow-key navigation direction. |
slotProps | TabsSlotProps | — | Per-slot overrides. |
sx | string | — | Root className shortcut. |
value | string | — | Controlled active value. |
variant | 'underline' | 'pill' | 'underline' | Visual style of the trigger strip — underline shows a bottom border under the active tab, pill renders active tabs as pills. |
<Tabs> is a compound component with 4 named slots. Each slot accepts a { className?: string } override via slotProps:
| Slot | Purpose |
|---|---|
root | Outer flex wrapper (list + panels). Direction driven by orientation. |
list | The trigger strip (role="tablist"). |
trigger | Every tab trigger button (role="tab"). Applies to all triggers uniformly. |
content | Every tab panel (role="tabpanel"). Applies to all panels uniformly. |
<Tabs
items={items}
sx="rounded-xl"
slotProps={{
list: { className: 'bg-neutral-100 p-2' },
trigger: { className: 'font-medium tracking-tight' },
content: { className: 'p-6' },
}}
/>Use sx for a root-level class override; use slotProps when you need to reach the list, trigger, or panel elements individually (e.g. bump trigger font-weight, add padding to every panel).
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 was rebuilt as a clean-room component on the
headless useTabs engine — the @radix-ui/react-tabs dependency was
removed. The public API is unchanged.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.dashforgePreset() — no dark: variants, so dark mode follows the CSS-var
swap automatically.Tabs. The @dashforge/ui Tabs mirrors this prop
surface — only the sx / slotProps override hooks differ between the two
skins.