Tabs
A tabbed interface for switching between related content panels.
import { ... } from '@corndev/ui' Overview panel — the default active tab.
All items.
HTML
<div class="corn-tabs" data-variant="line">
<div class="corn-tabs-tablist" role="tablist">
<button class="corn-tabs-tab" data-active role="tab" data-tab="one">One <span class="corn-tabs-tab-section" data-position="right">3</span></button>
<button class="corn-tabs-tab" role="tab" data-tab="two">Two</button>
</div>
<div class="corn-tabs-panel" role="tabpanel" data-tab="one">First panel</div>
<div class="corn-tabs-panel" role="tabpanel" data-tab="two" hidden>Second panel</div>
</div>
Classes
| Class |
|---|
.corn-tabs |
.corn-tabs-tablist |
.corn-tabs-tab |
.corn-tabs-panel |
.corn-tabs-tab-section |
Attributes
| On | Attribute | Values |
|---|---|---|
.corn-tabs | data-variant | line|pills |
.corn-tabs-tab | data-active | present or absent |
Vanilla JS
Required for behaviour — adds switches panels on click and arrow keys, and manages aria-selected/hidden.
import { createTabs } from '@corndev/ui';
import '@corndev/ui/theme';
const containerEl = document.querySelector('#target');
const instance = createTabs(containerEl, { /* options */ });
Options
| Option | Type | Default | Description |
|---|---|---|---|
defaultTab | string | undefined | data-tab of the initially active tab. Defaults to the first tab in the markup. |
variant | 'line'|'pills' | 'line' | Underlined tabs, or filled pills. |
onChange | (value: string) => void | undefined | Called with the newly active tab’s data-tab value. |
React
Tabs, Tab — from @corndev/ui-react.
Only the active panel is mounted, so switching destroys the others’ state. When that state cannot be lifted above Tabs — an editor’s undo history and cursor, a panel that queries on mount — mark that one Tab keepMounted: it is mounted the first time it is shown and kept, hidden, afterwards, so it is still not built until someone opens it. Tabs may be written out, mapped from a list, or both, including inside fragments. leftSection and rightSection put an icon or a count inside the tab button, beside the label; the vanilla equivalent is a .corn-tabs-tab-section span in your own markup. A child that is not a Tab is dropped, with a development warning.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | undefined | Active tab (controlled). Pair with onChange. Use it when the tab list itself changes — if the active tab stops existing, only the caller knows what to fall back to. |
defaultTab | string | undefined | value of the initially active tab (uncontrolled). Defaults to the first Tab. |
variant | 'line'|'pills' | 'line' | |
onChange | (value: string) => void | undefined | Called with the newly active tab’s value. |
className | string | undefined | Merged with the Corn UI classes. |
children | React.ReactNode | required | Tab elements. |
<Tab> props
A single tab. Never rendered directly — Tabs reads its props.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | required | Identifier, matched against defaultTab. |
label | React.ReactNode | required | Tab button content. |
disabled | boolean | false | |
keepMounted | boolean | false | Keep this panel mounted, hidden, once it has been shown, so its state survives a switch — a code editor’s text, undo history and cursor, or a panel that queries on mount. Mounted on first activation rather than up front, so an expensive panel is still not built until someone opens it. |
leftSection | React.ReactNode | undefined | Before the label inside the tab button — an icon. |
rightSection | React.ReactNode | undefined | After the label — a count or a badge. Both sections sit inside the button, so they must not be interactive. |
children | React.ReactNode|(() => React.ReactNode) | undefined | Panel body. A function is called only when the panel is mounted. |
Default
<Tabs>
<Tab value="overview" label="Overview">
Overview panel content.
</Tab>
<Tab value="details" label="Details">
Details panel content.
</Tab>
<Tab value="settings" label="Settings">
Settings panel content.
</Tab>
</Tabs>Pills variant
<Tabs variant="pills">
<Tab value="all" label="All">All items.</Tab>
<Tab value="active" label="Active">Active items only.</Tab>
<Tab value="archived" label="Archived">Archived items.</Tab>
</Tabs>Default active tab
<Tabs defaultTab="b">
<Tab value="a" label="Tab A">Panel A</Tab>
<Tab value="b" label="Tab B">Panel B — opened by default.</Tab>
<Tab value="c" label="Tab C">Panel C</Tab>
</Tabs>Keeping a panel mounted
Only the active panel is mounted — a tab is usually a whole view, and building five to show one is wasted work. The trade is that switching tabs destroys the other panels’ state.
When that state cannot live above Tabs — a code editor’s text, undo history and
cursor, or a panel that queries a database when it mounts — mark that tab
keepMounted:
<Tabs>
<Tab value="query" label="Query" keepMounted>
<SqlEditor />
</Tab>
<Tab value="results" label="Results">
<ResultsGrid />
</Tab>
</Tabs>
It is mounted the first time it is shown, then kept, hidden, from then on. So an expensive panel is still not built until someone opens it, and it survives every switch after that.
Tabs from a list
Tabs can be written out, mapped from data, or both — including inside a fragment:
<Tabs>
<Tab value="overview" label="Overview">…</Tab>
{environments.map(env => (
<Tab key={env.id} value={env.id} label={env.name}>…</Tab>
))}
</Tabs>
Icons and counts
leftSection and rightSection sit inside the tab button, beside the label:
<Tab value="issues" label="Issues" leftSection={<IconBug />} rightSection={<Badge>3</Badge>}>
…
</Tab>
They are inside a button, so they must not be interactive themselves. Without
React, put a .corn-tabs-tab-section span in your own tab button — the class is
the whole API.