Navigation
Tabs
Switch between related views that share the same space.
- Show scroll bars:
- When scrolling
- Click in the scroll bar to:
- Jump to the next page
- Default web browser:
- Safari
import { Tabs, TabsList, TabsPanel, TabsTab } from "macbetter/tabs"
const rows = {
general: [
["Show scroll bars", "When scrolling"],
["Click in the scroll bar to", "Jump to the next page"],
["Default web browser", "Safari"],
],
sharing: [
["Computer name", "Kyle’s MacBook Pro"],
["File Sharing", "On"],
["Screen Sharing", "Off"],
],
network: [
["Wi-Fi", "Connected to macbetter"],
["IP address", "192.168.1.24"],
["Firewall", "Active"],
],
} as const
export default function Example() {
return (
<Tabs defaultValue="general" style={{ width: "100%", maxWidth: 460 }}>
<TabsList aria-label="Settings">
<TabsTab value="general">General</TabsTab>
<TabsTab value="sharing">Sharing</TabsTab>
<TabsTab value="network">Network</TabsTab>
</TabsList>
{(Object.keys(rows) as (keyof typeof rows)[]).map((key) => (
<TabsPanel key={key} value={key}>
<dl style={{ display: "grid", gridTemplateColumns: "auto 1fr", gap: "10px 16px", margin: 0 }}>
{rows[key].map(([label, value]) => (
<div key={label} style={{ display: "contents" }}>
<dt className="mb-label-secondary" style={{ textAlign: "end" }}>{label}:</dt>
<dd style={{ margin: 0 }}>{value}</dd>
</div>
))}
</dl>
</TabsPanel>
))}
</Tabs>
)
}When to use it
Use a tab view to group a few related panes of controls, like the sections of a settings window, where each pane makes sense on its own. Keep the labels to a word or two and use nouns, not verbs.
Tabs are for content within a window. To switch between the top-level areas of an iPhone or iPad app, use a tab bar. To change how one set of content is shown, like a list or a grid, use a segmented control in the toolbar.
Keep the number of tabs small, usually six or fewer. If people need more, a sidebar or a pop-up button reads better.
Usage
import { Tabs, TabsList, TabsPanel, TabsTab } from "macbetter/tabs"Tab view
The default bordered variant is the macOS tab view: a segmented strip sits on the top edge of a recessed, framed box. The selected segment slides to the tab you choose.
- Show scroll bars:
- When scrolling
- Click in the scroll bar to:
- Jump to the next page
- Default web browser:
- Safari
import { Tabs, TabsList, TabsPanel, TabsTab } from "macbetter/tabs"
const rows = {
general: [
["Show scroll bars", "When scrolling"],
["Click in the scroll bar to", "Jump to the next page"],
["Default web browser", "Safari"],
],
sharing: [
["Computer name", "Kyle’s MacBook Pro"],
["File Sharing", "On"],
["Screen Sharing", "Off"],
],
network: [
["Wi-Fi", "Connected to macbetter"],
["IP address", "192.168.1.24"],
["Firewall", "Active"],
],
} as const
export default function Example() {
return (
<Tabs defaultValue="general" style={{ width: "100%", maxWidth: 460 }}>
<TabsList aria-label="Settings">
<TabsTab value="general">General</TabsTab>
<TabsTab value="sharing">Sharing</TabsTab>
<TabsTab value="network">Network</TabsTab>
</TabsList>
{(Object.keys(rows) as (keyof typeof rows)[]).map((key) => (
<TabsPanel key={key} value={key}>
<dl style={{ display: "grid", gridTemplateColumns: "auto 1fr", gap: "10px 16px", margin: 0 }}>
{rows[key].map(([label, value]) => (
<div key={label} style={{ display: "contents" }}>
<dt className="mb-label-secondary" style={{ textAlign: "end" }}>{label}:</dt>
<dd style={{ margin: 0 }}>{value}</dd>
</div>
))}
</dl>
</TabsPanel>
))}
</Tabs>
)
}Plain
variant="plain" drops the frame and the track. A soft capsule slides behind the selected label, with no underline, for tabs at the top of a page or a popover. Put decorative icons in TabsTabIcon.
import { Tabs, TabsList, TabsPanel, TabsTab, TabsTabIcon } from "macbetter/tabs"
import { ClockIcon, StarIcon, UsersIcon } from "lucide-react"
export default function Example() {
return (
<Tabs variant="plain" defaultValue="recents" style={{ width: "100%", maxWidth: 460 }}>
<TabsList aria-label="Contacts">
<TabsTab value="recents">
<TabsTabIcon>
<ClockIcon />
</TabsTabIcon>
Recents
</TabsTab>
<TabsTab value="favorites">
<TabsTabIcon>
<StarIcon />
</TabsTabIcon>
Favorites
</TabsTab>
<TabsTab value="groups">
<TabsTabIcon>
<UsersIcon />
</TabsTabIcon>
Groups
</TabsTab>
</TabsList>
<TabsPanel value="recents" className="mb-label-secondary">
Inès Moreau called 12 minutes ago. Kofi Mensah, yesterday.
</TabsPanel>
<TabsPanel value="favorites" className="mb-label-secondary">
Hollis Grant and Wren Takahashi are in your favorites.
</TabsPanel>
<TabsPanel value="groups" className="mb-label-secondary">
Design, Family and Climbing Club.
</TabsPanel>
</Tabs>
)
}Platform behavior
- macOS: a compact strip, 24px tall, centered on the panel's top edge, with hover feedback under a pointer.
- iPadOS and iOS: the strip stretches to the width of the content as a segmented control, 34pt tall, and the panel becomes an inset grouped surface below it. The selected segment gives a little under the finger.
- A vertical tab view (
orientation="vertical") puts the strip beside its panel on every platform.
Accessibility
- The list is a
tablist; each tab controls its panel, and each panel is labelled by its tab. Name the list witharia-label. - Tab moves focus into the list at the selected tab and out again on the next Tab. Left and Right move between tabs and select them, Home and End go to the ends, and right-to-left text swaps the arrows.
- Icons in
TabsTabIconstay out of the tab's name. An icon-only tab still needsaria-label.
API
Tabs
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "bordered" | "plain" | "bordered" | The framed macOS tab view, or tabs with no frame. |
defaultValue | string | — | The tab selected at first. Defaults to the first enabled tab. |
value | string | null | — | The selected tab, to control it. |
onValueChange | (value, details) => void | — | Runs when the selection wants to change. |
orientation | "horizontal" | "vertical" | "horizontal" | Which arrow keys move between tabs, and where the strip sits. |
activation | "automatic" | "manual" | "automatic" | Whether arrow keys select a tab or only focus it. |
TabsList
Renders the sliding indicator for you. Takes the props of a div; name it with aria-label.
TabsTab and TabsPanel
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Pairs a tab with the panel of the same value. |
disabled | boolean | — | On a tab: keeps it focusable but unselectable. |
keepMounted | boolean | — | On a panel: keeps it in the page while hidden, to preserve state. |
TabsTabIcon
| Prop | Type | Default | Description |
|---|---|---|---|
mirror | boolean | — | Flips a directional icon in right-to-left text. |
Data attributes and CSS variables
| Attribute or variable | On | Meaning |
|---|---|---|
data-variant | .mb-tabs | bordered or plain |
data-selected | .mb-tabs-tab, .mb-tabs-panel | The selected tab and its panel |
data-orientation | every part | horizontal or vertical |
data-disabled | .mb-tabs-tab | The tab can't be selected |
--mb-tabs-height | inherited | Height of the strip: 24px on macOS, 34pt on touch |
--mb-tabs-padding | inherited | Horizontal padding inside each tab |
--active-tab-left, --active-tab-width… | .mb-tabs-indicator | The selected tab's box, from ui-sdk |