Skip to main content

Tabs

Tabs

Tabbed interface for organizing content

Tags: component navigation layout

View Demo

Tabs provide a tabbed interface for organizing content into panels that can be switched between.

Import​

<script>
  import { Tabs, TabPanel } from '@classic-homes/theme-svelte';
</script>

Basic Usage​

<script>
  const tabs = [
    { id: 'account', label: 'Account' },
    { id: 'security', label: 'Security' },
    { id: 'notifications', label: 'Notifications' },
  ];
</script>

<Tabs {tabs}>
  <TabPanel id="account">
    <p>Account settings content</p>
  </TabPanel>
  <TabPanel id="security">
    <p>Security settings content</p>
  </TabPanel>
  <TabPanel id="notifications">
    <p>Notification preferences</p>
  </TabPanel>
</Tabs>

Props​

Tabs​

Prop Type Default Description
tabs Tab[] required Array of tab definitions
value string First tab ID Active tab ID (bindable)
id string Auto-generated Unique ID for ARIA
label string required Accessible name for the tab list (required)
collapse 'auto' | 'never' 'auto' What to do when the tabs don't fit (see Narrow Containers)
class string - Additional classes
children Snippet - Tab panel content
onValueChange (value: string) => void - Change callback

TabPanel​

Prop Type Default Description
id string - Tab ID this panel belongs to
children Snippet - Panel content
class string - Additional classes

Types​

interface Tab {
  id: string;
  label: string;
  disabled?: boolean;
}

Controlled Value​

Bind to the active tab:

<script>
  let activeTab = $state('account');
</script>

<Tabs {tabs} bind:value={activeTab}>
  <!-- Panels -->
</Tabs>

<p>Current tab: {activeTab}</p>

Change Handler​

React to tab changes:

<Tabs
  {tabs}
  onValueChange={(tabId) => {
    console.log('Switched to:', tabId);
    trackEvent('tab_change', { tab: tabId });
  }}
>
  <!-- Panels -->
</Tabs>

Narrow Containers​

When the tab row is wider than its container, Tabs shows a full-width Select listing the tabs instead. It measures the actual width, so this works the same on a full page, in a dialog or in a card, and it switches back to the row when there is room again. bind:value and onValueChange behave the same either way, and TabPanel wiring is unchanged.

Pass collapse="never" to always keep the row; it wraps onto more lines instead.

<Tabs {tabs} label="Filters" collapse="never">
  <!-- Panels -->
</Tabs>

Disabled Tabs​

<script>
  const tabs = [
    { id: 'basic', label: 'Basic' },
    { id: 'advanced', label: 'Advanced', disabled: true },
    { id: 'pro', label: 'Pro Features', disabled: !isPro },
  ];
</script>

<Tabs {tabs}>
  <!-- Panels -->
</Tabs>

Panel Styling​

<Tabs {tabs}>
  <TabPanel id="account" class="p-4 bg-muted/30 rounded-lg">
    <p>Styled panel content</p>
  </TabPanel>
</Tabs>

Examples​

Settings Page​

<script>
  const tabs = [
    { id: 'profile', label: 'Profile' },
    { id: 'account', label: 'Account' },
    { id: 'security', label: 'Security' },
    { id: 'billing', label: 'Billing' },
  ];
</script>

<Tabs {tabs}>
  <TabPanel id="profile">
    <Section title="Personal Information">
      <form class="space-y-4">
        <Input label="Display Name" />
        <Textarea label="Bio" />
        <Button>Save Changes</Button>
      </form>
    </Section>
  </TabPanel>

  <TabPanel id="account">
    <Section title="Email Settings">
      <Input label="Email Address" type="email" />
      <Button>Update Email</Button>
    </Section>
  </TabPanel>

  <TabPanel id="security">
    <Section title="Password">
      <form class="space-y-4">
        <Input label="Current Password" type="password" />
        <Input label="New Password" type="password" />
        <Button>Change Password</Button>
      </form>
    </Section>
  </TabPanel>

  <TabPanel id="billing">
    <Section title="Payment Methods">
      <p>Your payment methods will appear here.</p>
    </Section>
  </TabPanel>
</Tabs>

Product Details​

<script>
  const tabs = [
    { id: 'description', label: 'Description' },
    { id: 'specs', label: 'Specifications' },
    { id: 'reviews', label: 'Reviews' },
  ];
</script>

<Tabs {tabs}>
  <TabPanel id="description">
    <div class="prose">
      <p>{product.description}</p>
    </div>
  </TabPanel>

  <TabPanel id="specs">
    <DataTable data={product.specs} columns={specColumns} />
  </TabPanel>

  <TabPanel id="reviews">
    {#each reviews as review}
      <Card class="mb-4">
        <Card.Header>
          <div class="flex items-center gap-2">
            <Avatar size="sm">{review.author[0]}</Avatar>
            <span>{review.author}</span>
          </div>
        </Card.Header>
        <Card.Content>
          <p>{review.content}</p>
        </Card.Content>
      </Card>
    {/each}
  </TabPanel>
</Tabs>

Code Examples​

<script>
  const tabs = [
    { id: 'svelte', label: 'Svelte' },
    { id: 'react', label: 'React' },
    { id: 'vue', label: 'Vue' },
  ];
</script>

<Tabs {tabs}>
  <TabPanel id="svelte">
    <pre><code>{svelteCode}</code></pre>
  </TabPanel>
  <TabPanel id="react">
    <pre><code>{reactCode}</code></pre>
  </TabPanel>
  <TabPanel id="vue">
    <pre><code>{vueCode}</code></pre>
  </TabPanel>
</Tabs>

Keyboard Navigation​

  • ArrowLeft / ArrowRight - Move between tabs
  • Home - Go to first tab
  • End - Go to last tab
  • Enter / Space - Activate focused tab

Accessibility​

  • ARIA tablist, tab, and tabpanel roles
  • aria-selected on active tab
  • aria-controls linking tabs to panels
  • Roving tabindex for keyboard navigation
  • Disabled tabs are not focusable
  • When collapsed, the Select carries the tab list's label, and the hidden tab row keeps its ids so panels stay labelled
On this page