Tabbed interface for organizing content
Tags: component navigation layout
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 tabsHome- Go to first tabEnd- Go to last tabEnter/Space- Activate focused tab
Accessibility
- ARIA tablist, tab, and tabpanel roles
aria-selectedon active tabaria-controlslinking 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