SvTabs

A WAI-ARIA tabs widget with roving-tabindex arrow-key navigation, four tab positions, and optional closable tabs.

SvTabs renders a tab strip plus the active panel through a panel snippet. It is controlled - you own the active id via value and onChange - and every color comes from the grid's --sg-* tokens, so it matches the rest of the kit in light and dark. The keyboard behaviour, activation mode, and close handling live in a headless createTabs core; this component is one styled renderer over it.

Basic usage

<script lang="ts">
  import { SvTabs, type TabItem } from '@svgrid/grid'
  const tabs: TabItem[] = [
    { id: 'overview', label: 'Overview' },
    { id: 'activity', label: 'Activity' },
    { id: 'settings', label: 'Settings', disabled: true },
  ]
  let tab = $state('overview')
</script>

<SvTabs {tabs} value={tab} onChange={(id) => (tab = id)} variant="line">
  {#snippet panel(active)}
    {#if active === 'overview'}<Overview />{:else if active === 'activity'}<Activity />{/if}
  {/snippet}
</SvTabs>

Props

Prop Type Default Description
tabs ReadonlyArray<TabItem> - The tab items. See TabItem.
value string - Active tab id (bindable). Controlled via onChange.
onChange (id: string) => void - Fired when the active tab changes.
onClose (id: string) => void - Fired when a closable tab's x or the Delete key closes it.
tabPosition top | bottom | left | right top Where the tab strip sits.
orientation horizontal | vertical - Deprecated. Use tabPosition (horizontal -> top, vertical -> left).
variant line | pill line Underline strip or segmented pill look.
activation automatic | manual automatic Activate on focus, or on Enter / Space.
panel Snippet<[string]> - Renders the active panel; receives the active id.
ariaLabel string - Accessible name for the tab list.
dir EditorDir (ltr | rtl | auto) - Text direction. rtl mirrors layout and flips horizontal arrow keys.
reorderable boolean false Allow dragging tabs to reorder them.
onReorder (orderedIds: string[]) => void - Fired with the new id order after a drag.

TabItem

type TabItem = { id: string; label: string; disabled?: boolean; closable?: boolean }

Patterns

Pill view switcher

Use variant="pill" for a compact segmented control - ideal for switching a view without the weight of full tab chrome:

<SvTabs {tabs} value={view} onChange={(id) => (view = id)} variant="pill" ariaLabel="View" />

Closable tabs

Mark items closable and handle onClose to drop them from your array. The x button and the Delete key both fire it:

<SvTabs tabs={openDocs} value={active} onChange={(id) => (active = id)}
  onClose={(id) => (openDocs = openDocs.filter((t) => t.id !== id))} />

Side tabs

Set tabPosition="left" (or right) for a vertical strip in IDE-style layouts; arrow keys navigate along the strip's axis automatically.

Settings page

A full settings screen: one line strip switches between account, notification, and billing panels, each its own component, with the active id held in $state:

<script lang="ts">
  import { SvTabs, type TabItem } from '@svgrid/grid'
  const tabs: TabItem[] = [
    { id: 'account', label: 'Account' },
    { id: 'notifications', label: 'Notifications' },
    { id: 'billing', label: 'Billing' },
  ]
  let tab = $state('account')
</script>

<SvTabs {tabs} value={tab} onChange={(id) => (tab = id)} ariaLabel="Settings">
  {#snippet panel(active)}
    {#if active === 'account'}<AccountSettings />
    {:else if active === 'notifications'}<NotificationSettings />
    {:else}<BillingSettings />{/if}
  {/snippet}
</SvTabs>

Tip: the panel snippet renders only the active id, so a heavy panel mounts when its tab activates. Pair activation="manual" so arrow-keying past a panel waits for Enter / Space instead of eagerly mounting each one.

Accessibility

See also