navigation
Tabs
Compound WAI-ARIA tabs with underline, pills, and vertical variants, roving tabindex, and arrow key navigation.
Copied!
Examples
Underline
Default horizontal underline tabs.
Copied!
Manage your profile and sign-in methods.
Invoices, payment methods, and plan changes.
Invite teammates once this plan is upgraded.
<x-ui.tabs variant="underline" active="account">
<x-slot:list>
<x-ui.tabs.tab id="account">Account</x-ui.tabs.tab>
<x-ui.tabs.tab id="billing">Billing</x-ui.tabs.tab>
<x-ui.tabs.tab id="team" disabled>Team</x-ui.tabs.tab>
</x-slot:list>
<x-ui.tabs.panel id="account">Manage your profile and sign-in methods.</x-ui.tabs.panel>
<x-ui.tabs.panel id="billing">Invoices, payment methods, and plan changes.</x-ui.tabs.panel>
<x-ui.tabs.panel id="team">Invite teammates once this plan is upgraded.</x-ui.tabs.panel>
</x-ui.tabs>
<div
class="space-y-4"
x-data="{ active: 'account', select(id) { this.active = id } }"
>
<div role="tablist" aria-orientation="horizontal" class="flex gap-1 border-b border-border">
<button type="button" role="tab" id="account-tab" aria-controls="account-panel" x-bind:aria-selected="(active === 'account').toString()" x-bind:tabindex="active === 'account' ? 0 : -1" class="border-b-2 px-3 py-2 text-sm font-medium" x-bind:class="active === 'account' ? 'border-brand text-fg' : 'border-transparent text-fg-muted'" x-on:click="select('account')">Account</button>
<button type="button" role="tab" id="billing-tab" aria-controls="billing-panel" x-bind:aria-selected="(active === 'billing').toString()" x-bind:tabindex="active === 'billing' ? 0 : -1" class="border-b-2 px-3 py-2 text-sm font-medium" x-bind:class="active === 'billing' ? 'border-brand text-fg' : 'border-transparent text-fg-muted'" x-on:click="select('billing')">Billing</button>
</div>
<div role="tabpanel" id="account-panel" aria-labelledby="account-tab" x-show="active === 'account'" class="text-sm text-fg">Manage your profile and sign-in methods.</div>
<div role="tabpanel" id="billing-panel" aria-labelledby="billing-tab" x-show="active === 'billing'" x-cloak class="text-sm text-fg">Invoices, payment methods, and plan changes.</div>
</div>
Pills
Compact pill-style tablist.
Copied!
Project health and recent deploys.
Audit log entries for this workspace.
<x-ui.tabs variant="pills" active="overview">
<x-slot:list>
<x-ui.tabs.tab id="overview">Overview</x-ui.tabs.tab>
<x-ui.tabs.tab id="activity">Activity</x-ui.tabs.tab>
</x-slot:list>
<x-ui.tabs.panel id="overview">Project health and recent deploys.</x-ui.tabs.panel>
<x-ui.tabs.panel id="activity">Audit log entries for this workspace.</x-ui.tabs.panel>
</x-ui.tabs>
<div class="space-y-4" x-data="{ active: 'overview', select(id) { this.active = id } }">
<div role="tablist" class="inline-flex gap-1 rounded-control bg-surface-sunken p-1">
<button type="button" role="tab" x-bind:aria-selected="(active === 'overview').toString()" x-on:click="select('overview')" class="rounded-control px-3 py-1.5 text-sm" x-bind:class="active === 'overview' ? 'bg-surface-raised text-fg shadow-sm' : 'text-fg-muted'">Overview</button>
<button type="button" role="tab" x-bind:aria-selected="(active === 'activity').toString()" x-on:click="select('activity')" class="rounded-control px-3 py-1.5 text-sm" x-bind:class="active === 'activity' ? 'bg-surface-raised text-fg shadow-sm' : 'text-fg-muted'">Activity</button>
</div>
<div role="tabpanel" x-show="active === 'overview'" class="text-sm text-fg">Project health and recent deploys.</div>
<div role="tabpanel" x-show="active === 'activity'" x-cloak class="text-sm text-fg">Audit log entries for this workspace.</div>
</div>
Vertical
Vertical orientation with up/down arrow keys.
Copied!
Workspace name and default locale.
Session lifetime and SSO providers.
<x-ui.tabs variant="vertical" active="general">
<x-slot:list>
<x-ui.tabs.tab id="general">General</x-ui.tabs.tab>
<x-ui.tabs.tab id="security">Security</x-ui.tabs.tab>
</x-slot:list>
<x-ui.tabs.panel id="general">Workspace name and default locale.</x-ui.tabs.panel>
<x-ui.tabs.panel id="security">Session lifetime and SSO providers.</x-ui.tabs.panel>
</x-ui.tabs>
<div class="flex gap-6" x-data="{ active: 'general', select(id) { this.active = id } }">
<div role="tablist" aria-orientation="vertical" class="flex w-40 flex-col gap-1 border-r border-border pr-4">
<button type="button" role="tab" x-bind:aria-selected="(active === 'general').toString()" x-on:click="select('general')" class="rounded-control px-3 py-2 text-left text-sm" x-bind:class="active === 'general' ? 'bg-surface-sunken text-fg' : 'text-fg-muted'">General</button>
<button type="button" role="tab" x-bind:aria-selected="(active === 'security').toString()" x-on:click="select('security')" class="rounded-control px-3 py-2 text-left text-sm" x-bind:class="active === 'security' ? 'bg-surface-sunken text-fg' : 'text-fg-muted'">Security</button>
</div>
<div class="flex-1">
<div role="tabpanel" x-show="active === 'general'" class="text-sm text-fg">Workspace name and default locale.</div>
<div role="tabpanel" x-show="active === 'security'" x-cloak class="text-sm text-fg">Session lifetime and SSO providers.</div>
</div>
</div>
Props API
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | underline|pills|vertical | underline | Visual style and orientation. Vertical uses up/down arrows. |
| active | string|null | null | Initially selected tab id. Defaults to the first enabled tab. |
| tab.id | string | required | Stable id shared between the tab and its panel. |
| tab.disabled | bool | false | Prevents selection and skips the tab in keyboard traversal. |
Accessibility notes
Architected with WCAG 2.2 AA patterns in mind. These notes document the exact keyboard and ARIA behavior for this component.
- Implements role="tablist", role="tab", and role="tabpanel" with aria-controls and aria-labelledby pairing.
- Selected tab uses aria-selected="true" and tabindex="0". Other enabled tabs use tabindex="-1".
- Horizontal variants: ArrowLeft and ArrowRight move selection. Vertical variant: ArrowUp and ArrowDown.
- Home and End jump to the first and last enabled tabs. Disabled tabs are skipped.
- Only the active panel is shown. Panels remain in the DOM for stable ids.
Built by Praesepe
Architecture & custom software partner
Praesepe uses these exact UI primitives to ship SME dashboards, inventory systems, invoicing desks, and internal tools faster, without rebuilding a design system for every client.
- Same Blade + Tailwind 4 + Alpine 3 stack you see in this docs site
- Opinionated for Indonesian SME ops: invoices, stock, roles, approvals
- Architected with WCAG 2.2 AA patterns in mind for keyboard and ARIA