Tabs
PortedA row of tabs that switches views, with their TabsPanels or with the view the app renders. Tabs that do not fit scroll sideways: the ends fade, and buttons at the ends scroll by most of a width for a pointer (keys and touch scroll the list themselves). The selected tab scrolls into view when it mounts and when it changes.
import { Tabs, type TabsItem } from "@etched/monet/tabs";
const sections: TabsItem[] = [
{ value: "overview", label: "Overview" },
{ value: "runs", label: "Runs" },
{ value: "owners", label: "Owners" },
];
export function Preview() {
return <Tabs aria-label="Chip sections" tabs={sections} />;
}Installation
import { Tabs, TabsNav, TabsPanel } from "@etched/monet/tabs";Examples
Segmented and underline
import { Tabs, type TabsItem } from "@etched/monet/tabs";
const sections: TabsItem[] = [
{ value: "overview", label: "Overview" },
{ value: "runs", label: "Runs" },
{ value: "owners", label: "Owners" },
];
export function SegmentedAndUnderline() {
return (
<div className="grid">
<Tabs aria-label="Chip sections" tabs={sections} />
<Tabs aria-label="Chip sections, underline" variant="underline" tabs={sections} defaultValue="runs" />
</div>
);
}Small
import { Tabs, type TabsItem } from "@etched/monet/tabs";
const sections: TabsItem[] = [
{ value: "overview", label: "Overview" },
{ value: "runs", label: "Runs" },
{ value: "owners", label: "Owners" },
];
export function Small() {
return (
<div className="grid">
<Tabs aria-label="Chip sections, small" size="sm" tabs={sections} />
<Tabs aria-label="Chip sections, small underline" size="sm" variant="underline" tabs={sections} />
</div>
);
}Controlled view switch
Showing the pipeline view.
import { useState } from "react";
import { Tabs, type TabsItem } from "@etched/monet/tabs";
const views: TabsItem[] = [
{ value: "pipeline", label: "Pipeline" },
{ value: "summary", label: "Summary" },
];
export function ControlledViewSwitch() {
const [view, setView] = useState("pipeline");
return (
<div className="grid">
<Tabs aria-label="Role views" tabs={views} value={view} onValueChange={setView} />
<p>Showing the {view} view.</p>
</div>
);
}A disabled tab
import { Tabs, type TabsItem } from "@etched/monet/tabs";
const settings: TabsItem[] = [
{ value: "users", label: "Users" },
{ value: "notifications", label: "Notifications" },
{ value: "data", label: "Data quality", disabled: true },
];
export function ADisabledTab() {
return <Tabs aria-label="Settings" tabs={settings} />;
}Overflow
Tabs that do not fit scroll sideways, and their ends fade. Buttons at the ends scroll for a pointer. The selected tab scrolls into view when it mounts, as in the underline row, and when the app changes it.
import { useState } from "react";
import { Button } from "@etched/monet/button";
import { Tabs, type TabsItem } from "@etched/monet/tabs";
const many: TabsItem[] = ["Overview", "Runs", "Thermal", "Power", "Logs", "Owners", "History", "Alerts", "Settings"].map((label) => ({
value: label.toLowerCase(),
label,
}));
export function Overflow() {
const [view, setView] = useState("overview");
const at = many.findIndex((tab) => tab.value === view);
const move = (by: number) => setView(many[at + by]?.value ?? view);
return (
<div className="grid">
<Tabs aria-label="Chip views" tabs={many} value={view} onValueChange={setView} />
<div className="row">
<Button size="sm" disabled={at === 0} onClick={() => move(-1)}>
Previous view
</Button>
<Button size="sm" disabled={at === many.length - 1} onClick={() => move(1)}>
Next view
</Button>
</div>
<Tabs aria-label="Chip views, underline" variant="underline" tabs={many} defaultValue="alerts" />
</div>
);
}Vertical
A column of tabs beside its panels, such as settings sections. Up and Down move between the tabs.
Who can see and edit candidates.
Email and Slack alerts for new referrals.
import { Tabs, TabsPanel, type TabsItem } from "@etched/monet/tabs";
const sections: TabsItem[] = [
{ value: "overview", label: "Overview" },
{ value: "runs", label: "Runs" },
{ value: "owners", label: "Owners" },
];
const settings: TabsItem[] = [
{ value: "users", label: "Users" },
{ value: "notifications", label: "Notifications" },
{ value: "data", label: "Data quality", disabled: true },
];
export function Vertical() {
return (
<div className="grid" style={{ maxWidth: "36rem" }}>
<Tabs aria-label="Settings, vertical" orientation="vertical" variant="underline" tabs={settings.slice(0, 2)}>
<TabsPanel value="users">
<p>Who can see and edit candidates.</p>
</TabsPanel>
<TabsPanel value="notifications">
<p>Email and Slack alerts for new referrals.</p>
</TabsPanel>
</Tabs>
<Tabs aria-label="Chip sections, vertical" orientation="vertical" tabs={sections} defaultValue="runs" />
</div>
);
}Panels
TabsPanels inside Tabs are linked to their tabs and stay mounted while hidden: type a note, switch tabs, and come back. keepMounted={false} unmounts a hidden panel instead, for a heavy view.
import { InputArea } from "@etched/monet/input";
import { Tabs, TabsPanel } from "@etched/monet/tabs";
export function Panels() {
return (
<Tabs
aria-label="Candidate workspace"
variant="underline"
tabs={[
{ value: "notes", label: "Notes" },
{ value: "activity", label: "Activity" },
]}
>
<TabsPanel value="notes" className="grid">
<InputArea aria-label="Note" placeholder="Type a note, then switch tabs" rows={2} />
</TabsPanel>
<TabsPanel value="activity" keepMounted={false}>
<p>Oct 14 · Moved to Interviewing by Sam Lee</p>
</TabsPanel>
</Tabs>
);
}Links between pages
TabsNav is a nav of links with aria-current="page", for tabs that are URLs. Tab moves between the links.
import { TabsNav } from "@etched/monet/tabs";
export function LinksBetweenPages() {
return (
<div className="grid">
<TabsNav
aria-label="Settings sections"
variant="underline"
links={[
{ href: "#tabs", label: "Users", current: true },
{ href: "#tabs-notifications", label: "Notifications" },
{ href: "#tabs-data", label: "Data quality" },
]}
/>
<TabsNav
aria-label="Leaderboard period"
size="sm"
links={[
{ href: "#tabs-week", label: "Week" },
{ href: "#tabs-month", label: "Month", current: true },
{ href: "#tabs-quarter", label: "Quarter" },
]}
/>
</div>
);
}Accessibility
ArrowLeft/ArrowRight(ArrowUp/ArrowDownwhen vertical) move focus and wrap;Home/Endgo to the ends.EnterorSpaceselects the focused tab.- With
activateOnFocus, the arrow keys select as they move. A disabled tab hasaria-disabledand cannot be selected. - A
tablistnamed byaria-label, withtabs that havearia-selected. EachTabsPanelis atabpanelnamed by its tab. - The scroll buttons at the ends are
aria-hiddenand take no focus; the arrow keys reach every tab. TabsNavis anavof links, named by its requiredaria-label, witharia-current="page"on the current link. It has no tab roles.- Give
Tabsanaria-label. UseTabsNavwhen each tab opens its own URL.
API reference
TabsPanel
| Prop | Type | Default | Description |
|---|---|---|---|
| value * | string | – | The value of its tab. |
| children * | ReactNode | – | |
| keepMounted | boolean | true | Keeps the panel mounted while hidden, so what it holds (typed text, scroll, loaded data) is kept when the user comes back. false unmounts it, for a heavy view that should not stay in the page. |
Tabs
| Prop | Type | Default | Description |
|---|---|---|---|
| tabs * | TabsItem[] | – | |
| value | string | – | Selected tab, controlled. |
| defaultValue | string | the first tab | First selected tab, uncontrolled. |
| onValueChange | (value: string) => void | – | |
| activateOnFocus | boolean | false | Select a tab when the arrow keys move focus to it. Otherwise Enter or Space selects it. |
| variant | "segmented" | "underline" | "segmented" | |
| size | "base" | "sm" | "base" | |
| orientation | "horizontal" | "vertical" | "horizontal" | A row of tabs, or a column beside the panels. |
| aria-label | string | – | Names the tab list, for example "Role views". |
| children | ReactNode | – | TabsPanels, one per tab. Without them, the app renders the selected view itself. |