Skip to content

Tabs

Tabs is a single component. The tab buttons are declared as data via the tabs prop, and the active tab is tracked by its index with bind:tab.

import { Tabs } from '@delightstack/components';
import type { TabItem } from '@delightstack/components';

Each entry in tabs describes one tab button. Give it a content snippet to render that tab’s panel — label and content stay together in one place, and only the active panel is rendered.

Welcome to the overview panel. Get a quick summary of everything you need to know.

View code
<script>
import { Tabs } from '@delightstack/components';
let tab = $state(0);
</script>
{#snippet overview()}
<p>Welcome to the overview panel.</p>
{/snippet}
{#snippet features()}
<p>Explore the features.</p>
{/snippet}
{#snippet pricing()}
<p>View our pricing plans.</p>
{/snippet}
<Tabs bind:tab tabs={[
{ label: 'Overview', content: overview },
{ label: 'Features', content: features },
{ label: 'Pricing', content: pricing },
]} />

If you’d rather not write a content snippet per tab, omit it and pass children instead. The children render in the panel area and receive the active tab index, so you can gate them however you like:

<Tabs bind:tab tabs={[{ label: 'Inbox' }, { label: 'Sent' }]}>
{#if tab === 0}
<p>Your inbox.</p>
{:else if tab === 1}
<p>Your sent mail.</p>
{/if}
</Tabs>

The children snippet also receives { tab, select }, so {#snippet children({ tab, select })} works too if you want the current index or a programmatic select(i) helper.

Rounded pill-shaped tab buttons. The active pill fills with the action color.

Everything in one place.

View code
<Tabs bind:tab pills tabs={[
{ label: 'All' },
{ label: 'Active', badge: '3' },
{ label: 'Archived' },
]} />

An enclosed, segmented-control style group where the active tab rides on an elevated surface that glides between options.

The details panel.

View code
<Tabs bind:tab boxed tabs={[
{ label: 'Details' },
{ label: 'Activity' },
{ label: 'Settings' },
]} />

Set transition to animate the panel as the active tab changes. slide moves the content in the direction you navigated; fade cross-dissolves; none (the default) swaps instantly. All transitions respect prefers-reduced-motion.

Mercury

The smallest planet, closest to the Sun.

View code
<Tabs bind:tab transition="slide" tabs={[
{ label: 'Mercury', content: mercury },
{ label: 'Venus', content: venus },
{ label: 'Earth', content: earth },
]} />

Stack tabs for sidebar-style navigation. The list and the active panel sit side by side automatically — no wrapper layout needed.

General settings — your name, language, and time zone.

View code
<Tabs bind:tab orientation="vertical" tabs={[
{ label: 'General', content: general },
{ label: 'Security', content: security },
{ label: 'Notifications', content: notifications },
]} />

Give a tab a badge (a count or short string). Inactive badges are tinted; the active one inverts so it stays legible.

12 unread messages.

View code
<Tabs bind:tab tabs={[
{ label: 'Inbox', badge: 12 },
{ label: 'Sent' },
{ label: 'Drafts', badge: '2' },
]} />

Stretch tab buttons to fill the available width evenly.

The first panel.

View code
<Tabs bind:tab full_width tabs={[
{ label: 'First' },
{ label: 'Second' },
{ label: 'Third' },
]} />

Disable an individual tab with disabled: true on its entry, or disable the whole group with the container’s disabled prop. Disabled tabs are skipped during keyboard navigation.

This feature is ready to use.

View code
<Tabs bind:tab tabs={[
{ label: 'Available' },
{ label: 'Coming Soon', disabled: true },
{ label: 'Beta' },
]} />

Show shimmering placeholders while the real tabs load. The placeholders match the final list’s height so nothing shifts when content arrives.

View code
<Tabs skeleton={loading} skeleton_count={3} bind:tab tabs={tabs} />
PropTypeDefaultDescription
tabnumber0Index of the active tab ($bindable)
tabsTabItem[][]The tab buttons to render, in order. The array index is the tab’s value.
transition'none' | 'fade' | 'slide''none'How the panel animates between tabs
pillsbooleanfalseUse pill-shaped tab buttons
boxedbooleanfalseUse boxed / segmented-control tab style
orientation'horizontal' | 'vertical''horizontal'Tab list orientation
size'0' | '1' | '2' | '3''1'Size of the tabs
full_widthbooleanfalseStretch tabs to fill available width
disabledbooleanfalseDisable all tabs
skeletonbooleanfalseShow skeleton loading state
skeleton_countnumber3Number of skeleton tab placeholders
onchange(detail: { tab: number }) => voidundefinedCalled when the active tab changes
idstringautoElement ID
classstring''Additional CSS classes
childrenSnippet<[{ tab, select }]>undefinedPanel content used when a tab has no content snippet

Each entry in the tabs array:

FieldTypeDefaultDescription
labelstringThe tab button’s label text
badgestring | numberundefinedA badge shown after the label
disabledbooleanfalseDisable this individual tab
contentSnippetundefinedThis tab’s panel content
EventDetailDescription
onchange{ tab: number }Fires when the active tab index changes
interface TabItem {
label: string;
badge?: string | number;
disabled?: boolean;
content?: Snippet;
}
type TabsTransition = 'none' | 'fade' | 'slide';
  • The tab list uses role="tablist" with aria-orientation
  • Each button uses role="tab" with aria-selected and aria-controls
  • The panel uses role="tabpanel" with aria-labelledby pointing at the active tab
  • Arrow Left / Right (or Up / Down when vertical) moves between tabs
  • Home / End jumps to the first / last enabled tab
  • Enter / Space activates the focused tab
  • Roving tabindex keeps the tab list a single tab stop; disabled tabs are skipped
  • The sliding indicator and content transitions respect prefers-reduced-motion