Multi Tab Widgets
A thin shell for tabbed interactive widgets. The strip at the top names each tab; the body below renders the active tab's content — a chart, a viz, a form, anything. Controlled or uncontrolled; full tablist semantics; keyboard navigation built in.
Where this differs from StepWidgets: the entries are peers to switch between, not stations to walk through. No numbered pills, no progression metaphor — just tabs.
Preview
One channel, every same-origin tab.
Open a channel with the same name in every tab and posts fan out to every other tab on the same origin. The sender does not receive its own message — only peers do.
Customize
Installation
npx shadcn@latest add https://craftbits.dev/r/multi-tab-widgets.jsonUsage
import { MultiTabWidgets } from "@craft-bits/core";
<MultiTabWidgets
tabs={[
{ id: "broadcast", label: "BroadcastChannel", content: <BroadcastDemo /> },
{ id: "storage", label: "Storage Event", content: <StorageDemo /> },
{ id: "worker", label: "SharedWorker", content: <SharedWorkerDemo /> },
]}
/>Controlled mode — drive activeTab from outside:
const [tab, setTab] = useState("broadcast");
<MultiTabWidgets
activeTab={tab}
onActiveTabChange={(id) => setTab(id)}
tabs={[
{ id: "broadcast", label: "BroadcastChannel", content: <BroadcastDemo /> },
{ id: "storage", label: "Storage Event", content: <StorageDemo /> },
]}
/>Anatomy
- Tab strip — a
role="tablist"row (or column) of buttons. Each shows the tab'slabel. The active trigger picks up an accent-muted fill and a 2px accent underline. - Tab body — a single
role="tabpanel". Either passcontentper tab or passchildrento the whole component for a static panel. An optionaldescriptionrenders as a one-line caption above the body. - Orientation —
horizontal(default, strip on top) orvertical(strip on the left atmd+).
Props
| Prop | Type | Default | Description |
|---|---|---|---|
tabs | MultiTabWidgetsTab[] | required | Ordered tab descriptors. |
activeTab | string | — | Controlled active tab id. |
defaultActiveTab | string | first tab id | Initial tab in uncontrolled mode. |
onActiveTabChange | (id, index) => void | — | Fired on tab change. |
orientation | 'horizontal' | 'vertical' | 'horizontal' | Strip layout. |
renderTrigger | (args) => ReactNode | — | Override the trigger element. |
children | ReactNode | — | Static body, replaces the per-tab content. |
className | string | — | Merged onto the root via cn(). |
MultiTabWidgetsTab
| Field | Type | Description |
|---|---|---|
id | string | Stable identifier. |
label | ReactNode | Visible name on the strip. |
description | ReactNode | Optional caption rendered above the body when this tab is active. |
content | ReactNode | (index) => ReactNode | Body for this tab. |
Accessibility
- The strip is a
role="tablist"witharia-orientationset to match the layout. - Each trigger is a
role="tab"witharia-selected,aria-controls, and rovingtabindex. - The body is a
role="tabpanel"linked byaria-labelledbyto the active trigger. - Arrow keys move between triggers; Home and End jump to the first or last tab.
Credits
- Extracted from:
terminal-dreams(src/components/frontend-design/sdp-multi-tab/ui/MultiTabWidgets.tsx). The original was a bundle of twelve tab-coordination widgets — BroadcastChannel, leader election, conflict resolution, Web Locks, and more; this primitive keeps the tabbed shell and pushes every domain-specific concern back to the consumer's panel content.