Tab
Tabbed navigation for organizing content into switchable panels.
Playground
Account Settings
Manage your profile, email preferences, and security options.
<Tab
tier="modify"
defaultValue="account">
{#snippet tabs()}
<TabItem value="account">Account</TabItem>
<TabItem value="billing">Billing</TabItem>
<TabItem value="team">Team</TabItem>
{/snippet}
{#snippet panels()}
<TabPanel value="account">Manage your account settings</TabPanel>
<TabPanel value="billing">Update billing information</TabPanel>
<TabPanel value="team">Collaborate with your team</TabPanel>
{/snippet}
</Tab>01 Examples
Tab takes two snippets: tabs() for the strip and panels() for the content, and a TabItem pairs with its TabPanel by a matching value string. Drive the active tab with bind:value (or set an uncontrolled defaultValue), and onValueChange fires on every switch. An inactive panel
drops its content by default, so pass keepMounted to keep
a panel's state or lazy to defer its first render.
Settings sidebar
Settings
Profile
Update your display name, avatar, and bio. These details are visible to other team members.
<div class="border-border-subtle bg-surface-elevated overflow-hidden rounded-xl border">
<Tab variant="pills" orientation="vertical" bind:value={activeSettings}>
{#snippet tabs()}
<div class="w-48 shrink-0 p-3">
<p
class="text-text-tertiary text-3xs mb-3 px-2 font-semibold tracking-widest uppercase"
>
Settings
</p>
<TabItem value="profile">
{#snippet icon()}
<UserIcon size={16} />
{/snippet}
Profile
</TabItem>
<TabItem value="security">
{#snippet icon()}
<LockIcon size={16} />
{/snippet}
Security
</TabItem>
<TabItem value="notifications">
{#snippet icon()}
<BellIcon size={16} />
{/snippet}
Notifications
</TabItem>
<TabItem value="billing">
{#snippet icon()}
<CreditCardIcon size={16} />
{/snippet}
Billing
</TabItem>
</div>
{/snippet}
{#snippet panels()}
<div class="flex-1 p-5">
<TabPanel value="profile">
<h3 class="text-text-primary mb-1 text-base font-semibold">Profile</h3>
<p class="text-text-secondary text-sm">
Update your display name, avatar, and bio. These details are visible to other team
members.
</p>
</TabPanel>
<TabPanel value="security">
<h3 class="text-text-primary mb-1 text-base font-semibold">Security</h3>
<p class="text-text-secondary text-sm">
Enable two-factor authentication, manage active sessions, and review login
history.
</p>
</TabPanel>
<TabPanel value="notifications">
<h3 class="text-text-primary mb-1 text-base font-semibold">Notifications</h3>
<p class="text-text-secondary text-sm">
Choose which events trigger email, push, or in-app notifications.
</p>
</TabPanel>
<TabPanel value="billing">
<h3 class="text-text-primary mb-1 text-base font-semibold">Billing</h3>
<p class="text-text-secondary text-sm">
View your current plan, download invoices, and update payment methods.
</p>
</TabPanel>
</div>
{/snippet}
</Tab>
</div>Icons & Badges
Sarah Chen
Q4 Revenue Report
DevOps Bot
Deploy #1847 succeeded
Alex Rivera
Design review feedback
<Tab bind:value={activeProject}>
{#snippet tabs()}
<TabItem value="inbox">
{#snippet icon()}
<InboxIcon size={16} />
{/snippet}
Inbox
{#snippet badge()}
<Badge intent="danger" size="xs" variant="filled">3</Badge>
{/snippet}
</TabItem>
<TabItem value="drafts">
{#snippet icon()}
<EditIcon size={16} />
{/snippet}
Drafts
{#snippet badge()}
<Badge intent="neutral" size="xs" variant="soft">12</Badge>
{/snippet}
</TabItem>
<TabItem value="sent">
{#snippet icon()}
<SendIcon size={16} />
{/snippet}
Sent
</TabItem>
<TabItem value="archive">
{#snippet icon()}
<ArchiveIcon size={16} />
{/snippet}
Archive
</TabItem>
{/snippet}
{#snippet panels()}
<TabPanel value="inbox">
<div class="space-y-3 py-1">
{#each [{ from: 'Sarah Chen', subject: 'Q4 Revenue Report', time: '2m ago' }, { from: 'DevOps Bot', subject: 'Deploy #1847 succeeded', time: '15m ago' }, { from: 'Alex Rivera', subject: 'Design review feedback', time: '1h ago' }] as msg (msg.from + msg.subject)}
<div
class="border-border-subtle rounded-contain flex items-center gap-3 border p-3"
>
<div
class="bg-primary/10 text-primary flex size-8 shrink-0 items-center justify-center rounded-full text-xs font-bold"
>
{msg.from[0]}
</div>
<div class="min-w-0 flex-1">
<p class="text-text-primary truncate text-sm font-medium">{msg.from}</p>
<p class="text-text-tertiary truncate text-xs">{msg.subject}</p>
</div>
<span class="text-text-tertiary shrink-0 text-xs">{msg.time}</span>
</div>
{/each}
</div>
</TabPanel>
<TabPanel value="drafts">
<p class="text-text-secondary py-2 text-sm">12 drafts waiting to be sent.</p>
</TabPanel>
<TabPanel value="sent">
<p class="text-text-secondary py-2 text-sm">All sent messages appear here.</p>
</TabPanel>
<TabPanel value="archive">
<p class="text-text-secondary py-2 text-sm">Archived conversations for reference.</p>
</TabPanel>
{/snippet}
</Tab>Pricing switch
<Tab variant="solid" fullWidth defaultValue="monthly">
{#snippet tabs()}
<TabItem value="monthly">Monthly</TabItem>
<TabItem value="annual">Annual</TabItem>
{/snippet}
{#snippet panels()}
<TabPanel value="monthly">
<div class="flex items-baseline gap-1 py-3">
<span class="text-text-primary text-3xl font-bold">$29</span>
<span class="text-text-tertiary text-sm">/month</span>
</div>
</TabPanel>
<TabPanel value="annual">
<div class="flex items-baseline gap-1 py-3">
<span class="text-text-primary text-3xl font-bold">$19</span>
<span class="text-text-tertiary text-sm">/month</span>
<Badge intent="success" size="xs" variant="soft" class="ml-2">Save 34%</Badge>
</div>
</TabPanel>
{/snippet}
</Tab>Disabling a single tab
This tab is active and interactive.
<Tab variant="pills" defaultValue="a">
{#snippet tabs()}
<TabItem value="a">Active</TabItem>
<TabItem value="b" disabled>Locked</TabItem>
<TabItem value="c">Available</TabItem>
{/snippet}
{#snippet panels()}
<TabPanel value="a"
><p class="text-text-secondary text-sm">
This tab is active and interactive.
</p></TabPanel
>
<TabPanel value="b"><p class="text-text-tertiary text-sm">—</p></TabPanel>
<TabPanel value="c"
><p class="text-text-secondary text-sm">This tab is also interactive.</p></TabPanel
>
{/snippet}
</Tab>02 Customization
slotClasses Override
Custom rounded triggers inside a tinted pill container.
<Tab
variant="pills"
defaultValue="one"
slotClasses={{
list: 'bg-primary/5 rounded-xl p-1.5 gap-2',
trigger: 'rounded-xl',
panel: 'p-4 rounded-xl border border-border-subtle mt-3'
}}
>
{#snippet tabs()}
<TabItem value="one">Features</TabItem>
<TabItem value="two">Pricing</TabItem>
<TabItem value="three">FAQ</TabItem>
{/snippet}
{#snippet panels()}
<TabPanel value="one">
<p class="text-text-secondary text-sm">
Custom rounded triggers inside a tinted pill container.
</p>
</TabPanel>
<TabPanel value="two">
<p class="text-text-secondary text-sm">
The panel gets its own bordered card treatment.
</p>
</TabPanel>
<TabPanel value="three">
<p class="text-text-secondary text-sm">
All without leaving the component's API surface.
</p>
</TabPanel>
{/snippet}
</Tab>Glassmorphism (unstyled)
unstyled and hand-build the strip and panels through class, here into frosted-glass tabs. The data-[state=active] selector drives the active styling.<Tab unstyled defaultValue="home" class="w-full">
{#snippet tabs()}
<div class="flex gap-1 rounded-xl bg-white/10 p-1 backdrop-blur-md">
<TabItem
unstyled
value="home"
class="flex-1 rounded-lg px-4 py-2.5 text-center text-sm font-medium text-white/60 transition data-[state=active]:bg-white/20 data-[state=active]:text-white data-[state=active]:shadow-lg"
>
Home
</TabItem>
<TabItem
unstyled
value="explore"
class="flex-1 rounded-lg px-4 py-2.5 text-center text-sm font-medium text-white/60 transition data-[state=active]:bg-white/20 data-[state=active]:text-white data-[state=active]:shadow-lg"
>
Explore
</TabItem>
<TabItem
unstyled
value="library"
class="flex-1 rounded-lg px-4 py-2.5 text-center text-sm font-medium text-white/60 transition data-[state=active]:bg-white/20 data-[state=active]:text-white data-[state=active]:shadow-lg"
>
Library
</TabItem>
</div>
{/snippet}
{#snippet panels()}
<TabPanel
unstyled
value="home"
class="mt-4 rounded-xl bg-white/10 p-5 text-sm text-white/90 backdrop-blur-md"
>
Frosted glass panels over a gradient backdrop.
</TabPanel>
<TabPanel
unstyled
value="explore"
class="mt-4 rounded-xl bg-white/10 p-5 text-sm text-white/90 backdrop-blur-md"
>
Zero default styles. Every style here comes from the class props.
</TabPanel>
<TabPanel
unstyled
value="library"
class="mt-4 rounded-xl bg-white/10 p-5 text-sm text-white/90 backdrop-blur-md"
>
With <code class="rounded bg-white/15 px-1.5 py-0.5">unstyled</code>, the class props
own the entire look.
</TabPanel>
{/snippet}
</Tab>A tab chrome shared across the app belongs in BlocksProvider presets (presets.Tab, per-trigger styling under presets.TabItem, panel styling under presets.TabPanel), applied via preset instead of repeating slotClasses. See Customization.
03 Accessibility
Built-in ARIA
Uses role="tablist" on the tab strip, role="tab" on each trigger, and role="tabpanel" on content panels. aria-selected and aria-controls / aria-labelledby link tabs to their panels.
Keyboard Navigation
Arrow Left / Arrow Right cycle through horizontal tabs. Arrow Up / Arrow Down for vertical orientation. Home / End jump to first/last tab. Tab moves focus into and out of the tab strip.
Focus Management
Only the active tab has tabindex="0". Arrow keys move
focus and activate the tab (automatic activation pattern). Focus rings use focus-visible: so they never show on mouse clicks.
Reduced Motion
The sliding indicator respects prefers-reduced-motion, shortening or removing its
transition when the user has requested less motion.
04 API Reference
18 propsProp | Type | Default | Description | |
|---|---|---|---|---|
aria-label | string | — | Accessible name for the tab strip, applied to the inner role="tablist"
element — not the root wrapper, whose role-less <div> forbids
aria-label. Recommended whenever a page holds more than one tab strip;
screen readers announce tablists only by this name. | |
aria-labelledby | string | — | Id of a visible element that labels the tab strip. Like aria-label, it is
retargeted onto the inner role="tablist" element. Prefer this over
aria-label when a visible heading already names the strip. | |
class | string | — | Additional CSS class merged onto the root element. | |
defaultValue | string | — | Default active tab for uncontrolled mode.
Ignored when value is set. | |
disabled | boolean | false | Disables all tabs, preventing interaction and dimming the UI. | |
fullWidth | boolean | false | Stretch triggers to fill the available width. Only meaningful on horizontal tabs. | |
mint | MintProp | 'none' | Micro-interaction preset applied to each tab trigger (per-item via context). Only applies while the item is not disabled. | |
onValueChange | (value: string) => void | — | Fires when the active tab changes. Receives the new tab value string. | |
orientation | horizontalvertical | 'horizontal' | Tab strip axis. Determines whether triggers stack horizontally or vertically. | |
panels | Snippet | — | Snippet containing TabPanel components for the content area. | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ Tab: {...} }}>.
Prefer this over class overrides when the requested look falls outside the
semantic intent palette — presets keep hover/active/dark-mode logic coherent
and make the custom look reusable across the project. | |
size variant | lgmdsm | md | Controls the dimensions, padding, and text size of the Tab. Affects the component's physical footprint. Available options: lg, md, sm. | |
slotClasses | Partial<Record<TabSlots, string>> | — | Per-slot class overrides merged with (or replacing when unstyled) tv() styles. Slots: base | list | trigger | icon | label | badge | panel | indicator | |
tabs | Snippet | — | Snippet containing TabItem components for the tab strip. | |
tier | InteractiveTier | 'modify' | Semantic radius tier propagated to every TabItem. Default modify —
tabs read as navigation tap surfaces. Set to commit (or inherit via
TierContext from a wrapping <Toolbar tier="commit">) for full-pill
tab strips in marketing/product-overview contexts. Only the pills,
solid, and enclosed variants render a visible corner; line is
radius-agnostic. | |
unstyled | boolean | — | Strip all default tv() styles from the container and children. | |
value | string | — | Controlled active tab value. Use with bind:value for two-way binding.
When set, the component is controlled — you must handle onValueChange to update it. | |
variant variant | enclosedlinepillssolid | line | Controls the visual style and presentation of the Tab. Determines the component's visual treatment. Available options: enclosed, line, pills, solid. |
05 Types
Local type definitions used by this component.
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
RegisteredTab | interface | helper | 0 | One registered tab, as the tablist tracks it.
isDisabled is a getter rather than a boolean so the tablist reads the
item's live state instead of a snapshot: the item derives it from its own
disabled prop OR the group's, and both can change after registration.
Mirrors RegisteredSegment in SegmentGroup, which answers the same question. | |
TabContext | interface | helper | 0 | — | |
TabProps | type | helper | 0 | — | |
TabItemProps | interface | props | 0 | — | |
TabPanelProps | interface | props | 0 | — | |
TabVariants | type | variant | 0 | — | |
TabSlots | type | variant | 0 | Slot names derived from the tv() config — single source of truth for slotClasses. | |
MintProp | type | helper | 1 | — | |
InteractiveTier | type | helper | 1 | Semantic radius tier for interactive surfaces (3-tier system).
- commit → r-human (CTA, identity, status declarations)
- modify → r-interactive (fields, navigation, secondary actions)
Container components (Card, Alert, Toolbar surface, …) live in a third
tier contain (r-structure) which is **not** part of this propagation
context — those surfaces are always r-structure by design and have no
tier-flip use case. | |
MintName | type | helper | 0 | A mint name: a built-in (autocompleted), 'none' to disable, or any
consumer-registered name. (string & {}) keeps the registry open — a
custom name still type-checks, it just isn't suggested. A typo therefore
also still compiles (it resolves like an unregistered custom name and
warns at runtime); the union buys completion and docs, not validation. | |
MintConfig | interface | helper | 0 | — | |
BuiltinMintName | type | helper | 0 | Built-in mint names as a literal union, so the mint prop autocompletes
across every component — the single list the hand-curated playground knobs
and docs used to drift away from. |
06 Installation
Import
import { Tab, TabItem, TabPanel } from '@urbicon-ui/blocks';