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.json

Usage

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's label. The active trigger picks up an accent-muted fill and a 2px accent underline.
  • Tab body — a single role="tabpanel". Either pass content per tab or pass children to the whole component for a static panel. An optional description renders as a one-line caption above the body.
  • Orientationhorizontal (default, strip on top) or vertical (strip on the left at md+).

Props

PropTypeDefaultDescription
tabsMultiTabWidgetsTab[]requiredOrdered tab descriptors.
activeTabstringControlled active tab id.
defaultActiveTabstringfirst tab idInitial tab in uncontrolled mode.
onActiveTabChange(id, index) => voidFired on tab change.
orientation'horizontal' | 'vertical''horizontal'Strip layout.
renderTrigger(args) => ReactNodeOverride the trigger element.
childrenReactNodeStatic body, replaces the per-tab content.
classNamestringMerged onto the root via cn().

MultiTabWidgetsTab

FieldTypeDescription
idstringStable identifier.
labelReactNodeVisible name on the strip.
descriptionReactNodeOptional caption rendered above the body when this tab is active.
contentReactNode | (index) => ReactNodeBody for this tab.

Accessibility

  • The strip is a role="tablist" with aria-orientation set to match the layout.
  • Each trigger is a role="tab" with aria-selected, aria-controls, and roving tabindex.
  • The body is a role="tabpanel" linked by aria-labelledby to 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.