Skip to main content

Tabs

Switch between peer panels with managed focus and activation.

Use Tabs to switch between peer panels while staying in the same page context. Use navigation links when each destination needs its own URL and document state.

Basic usage#

import { Tabs } from 'moraine'

export function Example() {
  return <Tabs items={[{ value: 'account', label: 'Account', content: 'Profile settings' }]} />
}

Playground#

High-level metrics and system activity overview.

Props
Slots

Anatomy#

Tabs [component; slot=root]
├── list [slot]
│   ├── indicator [slot]
│   └── trigger [slot]
│       ├── leading [slot]
│       └── label [slot]
└── content [slot]

String labels use the label slot; JSX labels render directly inside the trigger.

Usage#

Selected value#

Use value with onChange for controlled tabs, or defaultValue for an initial local selection. Disabled items are skipped during selection and keyboard navigation.

For JSX panel content, use an item content getter so inactive panels are not constructed before they are rendered. This is especially important in SSR and MDX, where eagerly creating an omitted component can consume hydration keys that have no matching HTML.

import { Tabs } from 'moraine'
import type { TabsT } from 'moraine'
import { createSignal } from 'solid-js'

const TAB_ITEMS: TabsT.Item[] = [
  {
    value: 'account',
    label: 'Account',
    icon: 'i-lucide:user',
    content: (
      <div class="text-xs text-muted-foreground mt-2 p-3 b-1 b-border rounded-lg bg-card/40">
        Manage your account preferences, profile details, and email notifications.
      </div>
    ),
  },
  {
    value: 'security',
    label: 'Security',
    icon: 'i-lucide:shield',
    content: (
      <div class="text-xs text-muted-foreground mt-2 p-3 b-1 b-border rounded-lg bg-card/40">
        Configure two-factor authentication, active sessions, and API tokens.
      </div>
    ),
  },
  {
    value: 'billing',
    label: 'Billing',
    icon: 'i-lucide:credit-card',
    content: (
      <div class="text-xs text-muted-foreground mt-2 p-3 b-1 b-border rounded-lg bg-card/40">
        View past invoices, payment methods, and current subscription usage.
      </div>
    ),
  },
]

export function SelectionUsage() {
  const [tab, setTab] = createSignal('account')

  return (
    <div class="max-w-md w-full space-y-3">
      <Tabs items={TAB_ITEMS} value={tab()} onChange={setTab} />
      <p class="text-xs text-muted-foreground">
        Active tab: <span class="text-foreground font-medium font-mono">{tab()}</span>
      </p>
    </div>
  )
}

Activation and keyboard navigation#

activationMode="automatic" selects the tab while arrow navigation moves focus. In manual mode, arrows move focus and Enter or Space selects. loop controls whether navigation wraps at the first and last enabled items.

import { Tabs } from 'moraine'
import type { TabsT } from 'moraine'

const TABS: TabsT.Item[] = [
  {
    value: 'tab1',
    label: 'Overview',
    content: (
      <div class="text-xs text-muted-foreground p-3">System status and resource metrics.</div>
    ),
  },
  {
    value: 'tab2',
    label: 'Analytics',
    content: (
      <div class="text-xs text-muted-foreground p-3">Traffic breakdowns and request volume.</div>
    ),
  },
  {
    value: 'tab3',
    label: 'Logs',
    content: (
      <div class="text-xs text-muted-foreground p-3">Real-time application execution stream.</div>
    ),
  },
]

export function ActivationUsage() {
  return (
    <div class="max-w-md w-full space-y-4">
      <div class="space-y-1.5">
        <span class="text-xs text-muted-foreground font-medium">
          Automatic activation (focus selects tab)
        </span>
        <Tabs items={TABS} activationMode="automatic" defaultValue="tab1" />
      </div>
      <div class="space-y-1.5">
        <span class="text-xs text-muted-foreground font-medium">
          Manual activation (Enter / Space selects tab)
        </span>
        <Tabs items={TABS} activationMode="manual" defaultValue="tab1" />
      </div>
    </div>
  )
}

Keyboard interaction#

Arrow keys follow the configured orientation. activationMode="automatic" selects while moving focus; manual activation waits for Enter or Space.

Key Description
Arrow keys Moves focus among enabled tabs by orientation.
↖ / ↘ Moves focus to the first or last enabled tab.
↵ / Space Activates the focused tab in manual mode.
⇥ Leaves the tab list according to normal focus order.

Examples#

Activation mode#

import { Tabs } from 'moraine'
import type { TabsT } from 'moraine'

const SETTINGS_TABS: TabsT.Item[] = [
  {
    label: 'General',
    value: 'general',
    icon: 'i-lucide:sliders',
    content: (
      <div class="p-3 b-1 b-border rounded-xl bg-card/40 space-y-2">
        <h4 class="text-sm font-semibold">General Preferences</h4>
        <p class="text-xs text-muted-foreground">
          Automatic activation triggers immediately on arrow navigation.
        </p>
      </div>
    ),
  },
  {
    label: 'Deployments',
    value: 'deployments',
    icon: 'i-lucide:rocket',
    content: (
      <div class="p-3 b-1 b-border rounded-xl bg-card/40 space-y-2">
        <h4 class="text-sm font-semibold">Deployment Settings</h4>
        <p class="text-xs text-muted-foreground">
          Manual activation requires pressing Enter or Space to commit selection.
        </p>
      </div>
    ),
  },
  {
    label: 'Notifications',
    value: 'notifications',
    icon: 'i-lucide:bell',
    content: (
      <div class="p-3 b-1 b-border rounded-xl bg-card/40 space-y-2">
        <h4 class="text-sm font-semibold">Alert Rules</h4>
        <p class="text-xs text-muted-foreground">Configure webhook dispatch channels.</p>
      </div>
    ),
  },
]

export function ActivationMode() {
  return (
    <div class="gap-6 grid max-w-2xl sm:grid-cols-2">
      <div class="space-y-2">
        <span class="text-xs text-muted-foreground tracking-wider font-semibold uppercase">
          Automatic Activation
        </span>
        <Tabs items={SETTINGS_TABS} activationMode="automatic" defaultValue="general" />
      </div>
      <div class="space-y-2">
        <span class="text-xs text-muted-foreground tracking-wider font-semibold uppercase">
          Manual Activation (Enter / Space)
        </span>
        <Tabs items={SETTINGS_TABS} activationMode="manual" defaultValue="general" />
      </div>
    </div>
  )
}

Attributes#

Attributes
data-disabledSlot: tabs, tabs-triggerDescription: Present when the component, slot, or item is disabled.
data-highlightedSlot: tabs-triggerDescription: Present when the item is highlighted by pointer or keyboard navigation.
data-selectedSlot: tabs-trigger, tabs-contentDescription: Present when the item or tab is selected.

Props#

Props for the Tabs component. Renders a <div> element by default.

Prop