Tabs
One set of panels, one of which is shown. Horizontal or vertical.
import { Tab, TabPanel, Tabs } from 'neba';
<Tabs defaultValue="overview">
<Tab value="overview">Overview</Tab>
<Tab value="usage">Usage</Tab>
<TabPanel value="overview">Three deploys today, all green.</TabPanel>
<TabPanel value="usage">1,284 build minutes used.</TabPanel>
</Tabs>;There is no <TabList> wrapper. Everything written between the tags is either a tab or a panel, the two go in different boxes, and the component sorts them — rather than adding a wrapper you have to remember.
Props
Tabs
| Prop | Type | Default | Description |
|---|---|---|---|
| variantshared | 'solid' | 'outline' | 'text' | 'outline' | Weight of the tab **bar**, not of the panels. solid is a segmented control, outline is a rule along the bar, text is the same without it |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | The tabs' height and type scale, on Button's own ladder — so a tab bar keeps its baseline in a toolbar |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | 'default' | 'compact' | 'default' | Padding only — never the height, never the type scale |
| value | string | number | null | — | The chosen tab, for a controlled set |
| defaultValue | string | number | null | — | Which starts chosen |
| onValueChange | (value: string | number | null) => void | — | Called when the chosen tab changes |
| orientationshared | 'horizontal' | 'vertical' | 'horizontal' | Which way the bar runs. vertical stands the tabs down the side and moves the arrow keys onto the other axis |
| activateOnFocus | boolean | false | Whether the arrow keys also choose. Off by default: the moment one panel fetches, walking past four tabs fires four requests |
| loopFocus | boolean | true | Whether the arrow keys wrap from the last tab back to the first |
| fullWidth | boolean | false | The tabs share the full width of the bar, each taking an equal part |
| children | ReactNode | — | The Tabs and the TabPanels. They are sorted into their two boxes for you, so there is no list wrapper to remember |
Tab
| Prop | Type | Default | Description |
|---|---|---|---|
| value * | string | number | — | Identifies the tab, and picks out the panel with the same value |
| startIcon | ReactNode | — | Content before the label |
| endIcon | ReactNode | — | Content after the label — a count, a Badge, a status dot |
| disabled | boolean | false | Unavailable, but still listed |
| children | ReactNode | — | The tab's label |
TabPanel
| Prop | Type | Default | Description |
|---|---|---|---|
| value * | string | number | — | Which tab shows this panel |
| keepMounted | boolean | false | Keeps the panel in the DOM while hidden. For content that is expensive to build, or form state that should survive |
| children | ReactNode | — | The content |
Examples
Variants
variant here is the weight of the tab bar, not of the panels under it.
solid— a segmented control. The bar is a frosted trough and the indicator is a filled tile that slides between the tabs.outline— the classic. A rule along the edge of the bar, with the indicator riding on it.text— the same bar with the rule taken away. For tabs inside a Card that already has an edge of its own.
Orientation
A vertical bar is not a horizontal one turned on its side. Base UI moves the arrow keys onto the other axis with it, which is the part that makes a vertical tab bar reachable at all.
Icons and counts
The indicator moves its box
Base UI measures the chosen tab and writes the result onto --active-tab-left, --active-tab-width and their siblings. The indicator animates left/top and width/height from those.
That is a layout animation on an empty box, not a transform on a label — nothing with text in it moves, which is the line the no-transform rule actually draws.
It is also the one place in the library that reaches for a physical property (left) instead of a logical one, on purpose. --active-tab-left is a measurement: the distance in pixels from the list's left edge to the chosen tab's, and it stays a distance from the left under RTL. Pairing a physical measurement with a logical property is what would break the direction, not what would fix it. The edge the bar sits on is logical, because that one genuinely flips.
Why activateOnFocus is off by default
Automatic activation is only kind when every panel is already on the page. The moment one of them fetches, walking past four tabs with the arrow keys fires four requests.
Tabs are on the control ladder
A md tab and a md Button are the same 32px. That is what lets a tab bar sit in a toolbar next to a button without the row losing its baseline.
Accessibility
Base UI owns everything that makes a tab bar a tab bar rather than a row of buttons: roving focus so the whole bar is one tab stop, the arrow keys on whichever axis the bar runs, Home and End, the tab / tabpanel roles and the aria-controls wiring between them, and the measurement that puts the indicator under the chosen tab.
A panel with nothing focusable inside it takes focus itself, so the content is reachable by keyboard — and when it does, it gets the house focus ring rather than the browser's outline.