Tabs
A row of tabs that each show one TabPanel.
import { TabPanel, Tabs } from '@zitdevs/ui';Examples
Section titled “Examples”These demos are server HTML with no React on the page. The page runs one script,
enhanceTabs(document) from @zitdevs/ui/enhance, exactly as the Astro website does. Click a tab,
or focus one and use the arrow keys, Home and End.
<Tabs label="Filter resources" defaultValue="tab-guides" items={[ { id: 'tab-all', label: 'All', count: '24', panelId: 'panel-all' }, { id: 'tab-guides', label: 'Guides', count: '8', panelId: 'panel-guides' }, ]}/><TabPanel id="panel-all" tabId="tab-all">…</TabPanel><TabPanel id="panel-guides" tabId="tab-guides" selected>…</TabPanel>Controlled
Section titled “Controlled”A caller that holds the selection passes value and onValueChange: a click asks, and the tab
changes when value does. The panels may come from anywhere — a Server Component’s panels follow
too. See Interactive components for a live example.
'use client';
const [tab, setTab] = useState('tab-all');
<Tabs label="Filter resources" items={items} value={tab} onValueChange={setTab} />;One grid, several filters
Section titled “One grid, several filters”Tabs that filter one grid may all point at the same panel. It stays visible, named by whichever tab
is selected, and the page filters it on zd:tabchange:
import { TAB_EVENT, type TabChangeDetail } from '@zitdevs/ui/enhance';
grid.ownerDocument.addEventListener(TAB_EVENT, (event) => { const { tabId } = (event as CustomEvent<TabChangeDetail>).detail; // show the cards of tabId});| Prop | Type | Default | Notes |
|---|---|---|---|
label |
string |
required | Names the tab list, such as “Filter resources”. |
items |
readonly TabItem[] |
required | { id, label, count?, panelId }. count is read as part of the tab’s name. |
defaultValue |
string |
first tab | The tab id selected at first. A new one starts the list over from it. |
value |
string |
— | The selected tab id, for a caller that holds the selection. |
onValueChange |
(tabId: string) => void |
— | The tab the reader asks for. Uncontrolled, it reports a change; controlled, it asks for one. |
size |
'md' | 'lg' |
'md' |
Tabs 40px / 52px tall. |
surface |
'light' | 'dark' |
'light' |
dark for the inverse and the primary band. |
ref |
Ref<HTMLDivElement> |
— | The tab list. |
className |
string |
— | Placement, on the tab list. |
| anything else | <div> attributes |
— | On the tab list. |
Exported types: TabsProps, TabItem. From @zitdevs/ui/enhance: enhanceTabs, TAB_EVENT,
TabChangeDetail, TabsOptions, TabsEnhancement.
Accessibility
Section titled “Accessibility”- The ARIA tabs pattern with automatic activation: a click or an arrow key selects a tab and shows its panel. Left and Right wrap; Home and End go to the ends.
- Only the selected tab is in the tab order (
tabindex="0"); the others are-1, so Tab moves from the list straight into the panel. - Each tab names its panel with
aria-controls, and the panel is named by its tab. - Every tab is a
<button type="button">: it never submits a form. - The row scrolls sideways when it does not fit, so the focus ring is drawn inwards, where the scroll container cannot clip it. Forced-colours modes paint its outline.
Without JavaScript
Section titled “Without JavaScript”The server renders the first state: the selected tab, and its panel shown, with the others in the
document but hidden. Until an enhancer runs, the other tabs do nothing. Where every panel must be
reachable with no script at all, give each view its own URL instead (?tab=guides) and render the
links.
Switching needs the enhancer, written once and run two ways:
- React: nothing to do. The tab list is a small client part that runs the enhancer once it has
hydrated. In Next.js,
Tabsworks from a Server Component; only that part ships to the browser. - No React (Astro, server templates): load
@zitdevs/ui/enhanceand callenhanceTabs(document)once. It finds every[data-tabs]list and enhances each one once.
Theming
Section titled “Theming”muted, ink, primary and border-default on a light field; on-inverse-quiet, on-inverse,
accent and primary-inverse on a dark one; the inset focus ring.
Use instead: several answers open at once → Accordion.