Skip to content
Praesepe UI

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

Component props
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

Talk to the studio

WhatsApp: TODO Email the studio Visit Praesepe