Skip to main content
Urbicon UI

Tab

Tabbed navigation for organizing content into switchable panels.

Playground

Account Settings

Manage your profile, email preferences, and security options.

Variant Style variant
Size Style variant
Tier
Orientation
Mint
<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

Sidebar-style navigation for settings pages and multi-section forms.

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

Leading icons for visual scanning, trailing badges for status indicators.
S

Sarah Chen

Q4 Revenue Report

2m ago
D

DevOps Bot

Deploy #1847 succeeded

15m ago
A

Alex Rivera

Design review feedback

1h ago
<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

Tabs stretch equally across the container, useful for mobile layouts and modal headers.
$29 /month
<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

A disabled TabItem is skipped by roving keyboard navigation while the rest stay interactive.

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

Style individual slots: list background, panel padding, trigger rounding.

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)

Drop the tokens with unstyled and hand-build the strip and panels through class, here into frosted-glass tabs. The data-[state=active] selector drives the active styling.
Frosted glass panels over a gradient backdrop.
<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 props
18 props
Prop
Type
Default
Description

05 Types

Local type definitions used by this component.

12 types
Name
Kind
Category
Used by
Description

06 Installation

Import

import { Tab, TabItem, TabPanel } from '@urbicon-ui/blocks';