TabGroup

Organism

Accessible tab navigation following the role="tablist" / role="tab" / role="tabpanel" ARIA pattern. Arrow-key navigation; tabIndex=-1 on inactive tabs. Optional per-tab isolation (error boundary + Suspense), `order` and per-tab `lazy` for tabs other modules contribute.

Default

Preview

Profile settings content.

Code
<TabGroup
  label="Account settings"
  tabs={[
    { id: 'profile', label: 'Profile', content: <ProfileSettings /> },
    { id: 'security', label: 'Security', content: <SecuritySettings /> },
    { id: 'billing', label: 'Billing', content: <BillingSettings /> },
  ]}
/>

Icons + badge + disabled

Preview

Overview content.

Code
<TabGroup tabs={[
  { id: 'overview', label: 'Overview', icon: <BarChart2 />, content: ... },
  { id: 'analytics', label: 'Analytics', icon: <TrendingUp />, badge: <Badge>New</Badge>, content: ... },
  { id: 'settings', label: 'Settings', disabled: true, content: ... },
]} />

Lazy panels

Preview

Always mounted (initial).

Mount log: Tab 1 mounted

Code
// With lazy=true, panel content is only rendered when first activated:
<TabGroup lazy tabs={[
  { id: 'heavy', label: 'Heavy', content: <HeavyComponent /> },
]} />

Contributed tabs: isolate + order

Preview

Host tab: contact details.

Reported: nothing yet (open Calls)

Code
// Each isolated panel sits in an error boundary + Suspense: a throwing tab shows a
// "failed" placeholder with Retry (or renderError), the others keep working.
<TabGroup
  isolate
  onTabError={(tabId, error) => report(tabId, error)}
  tabs={[
    { id: 'overview', label: 'Overview', content: <Overview /> },             // host: order 0
    { id: 'tasks', label: 'Tasks', order: 10, lazy: true, content: <Tasks /> },
    { id: 'calls', label: 'Calls', order: 20, lazy: true, content: <CallLog /> },
  ]}
/>
Sourcemodules/ui/TabGroup.tsx