Tab
Tabbed navigation for organizing content into switchable panels. Supports four visual variants, vertical orientation, icons, badges, and full keyboard navigation.
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
Vertical Orientation
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 defaultValue="inbox" 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>Full Width
<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>Disabled Tabs
All disabled
All interaction is disabled.
Single tab disabled
This tab is active and interactive.
<div class="flex flex-col gap-2.5">
<p class="text-text-tertiary text-xs font-medium tracking-wider uppercase">All disabled</p>
<Tab disabled defaultValue="a">
{#snippet tabs()}
<TabItem value="a">Account</TabItem>
<TabItem value="b">Billing</TabItem>
<TabItem value="c">Team</TabItem>
{/snippet}
{#snippet panels()}
<TabPanel value="a"
><p class="text-text-tertiary text-sm">All interaction is disabled.</p></TabPanel
>
<TabPanel value="b"><p class="text-text-tertiary text-sm">—</p></TabPanel>
<TabPanel value="c"><p class="text-text-tertiary text-sm">—</p></TabPanel>
{/snippet}
</Tab>
</div>
<div class="flex flex-col gap-2.5">
<p class="text-text-tertiary text-xs font-medium tracking-wider uppercase">
Single tab disabled
</p>
<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>
</div>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)
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-all 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-all 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-all 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 with gradient backdrop. The <code
class="rounded bg-white/15 px-1.5 py-0.5">data-[state=active]</code
> selector drives the active styling.
</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 — everything is hand-crafted via 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"
>
Proof that <code class="rounded bg-white/15 px-1.5 py-0.5">unstyled</code> gives full creative
freedom.
</TabPanel>
{/snippet}
</Tab>Terminal (unstyled)
$ bun run build
✓ 247 modules transformed.
✓ built in 1.23s
Process exited with code 0
<Tab unstyled defaultValue="stdout" class="w-full font-mono">
{#snippet tabs()}
<div class="flex gap-px border-b border-emerald-500/20">
<TabItem
unstyled
value="stdout"
class="border-b-2 border-transparent px-4 py-2 text-xs text-emerald-300 transition-colors data-[state=active]:border-emerald-400 data-[state=active]:text-emerald-400"
>
stdout
</TabItem>
<TabItem
unstyled
value="stderr"
class="border-b-2 border-transparent px-4 py-2 text-xs text-emerald-300 transition-colors data-[state=active]:border-emerald-400 data-[state=active]:text-emerald-400"
>
stderr
</TabItem>
<TabItem
unstyled
value="logs"
class="border-b-2 border-transparent px-4 py-2 text-xs text-emerald-300 transition-colors data-[state=active]:border-emerald-400 data-[state=active]:text-emerald-400"
>
logs
</TabItem>
</div>
{/snippet}
{#snippet panels()}
<TabPanel unstyled value="stdout" class="py-4 text-xs leading-relaxed text-emerald-200">
<p>$ bun run build</p>
<p class="text-emerald-400">✓ 247 modules transformed.</p>
<p class="text-emerald-400">✓ built in 1.23s</p>
<p class="mt-2 text-emerald-300">Process exited with code 0</p>
</TabPanel>
<TabPanel unstyled value="stderr" class="py-4 text-xs leading-relaxed text-red-400/80">
<p>No errors.</p>
</TabPanel>
<TabPanel unstyled value="logs" class="py-4 text-xs leading-relaxed text-emerald-200">
<p class="text-emerald-400">[12:34:01] Starting compilation...</p>
<p class="text-emerald-400">[12:34:02] Bundling assets...</p>
<p class="text-emerald-300">[12:34:03] Done.</p>
</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 and Mint effects respect prefers-reduced-motion. Transitions are shortened or
removed when the user has requested less motion.
04 API Reference
Prop | Type | Default | Description | |
|---|---|---|---|---|
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 | |
|---|---|---|---|---|---|
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';