Tabs
Sectioned content switching with a full WAI-ARIA tablist: three visual treatments, keyboard-driven.
Live demo
Tabs presents one panel of content at a time, switched by a row of tab triggers. It implements the complete WAI-ARIA tabs pattern: a role="tablist" of role="tab" buttons paired with role="tabpanel" regions, roving tabindex (only the selected tab is in the tab order), arrow-key navigation with Home/End jumps, and aria-selected / aria-controls / aria-labelledby wiring done for you.
The activation knob chooses automatic activation (arrowing selects as you move) or manual (arrow to move focus, Enter/Space to select). Three visual treatments (underline, pill, and enclosed) change the chrome without touching the semantics. Authors declare each tab as a slot="tab" element and its content as the matching slot="panel" element; the element pairs them by order, assigns ids, and owns the selection state.
When to use
How this component composes with the rest of the set.
Props
8 props, straight from the manifest.
| Prop | Type | Default | Bindings | Description |
|---|---|---|---|---|
Appearance
Variants
underline
A minimal row with a moving underline under the selected tab. The default.
pill
Tabs sit in a tinted track; the selected tab becomes a solid accent pill.
enclosed
Folder-style tabs that connect to the panel; the selected tab joins the content surface.
Sizes
sm
Compact.
md
Default.
States
selected
The active tab. Tone-colored text, plus the variant's selection chrome (underline / pill / folder). When the theme's --selection-cue resolves to marker (a high-contrast or redundant-cues algorithm), the selected tab gains a non-color check glyph so selection never rests on color alone.
hover
Pointer over an unselected tab; overlay paints the hover tint and the text brightens.
active
Tab pressed. Overlay paints the press tint.
focus-visible
Keyboard focus on a tab or panel. A token-colored ring, plus a transparent outline that becomes real in forced-colors mode.
disabled
A non-selectable tab. Muted ink, overlay suppressed, skipped by arrow-key navigation.
Anatomy
The named parts that make up the component, with their selectors.
tabs
The root wrapper carrying the variant and size classes; stacks the tablist over the active panel.
tablist
The horizontal row of tab triggers (role=tablist).
tab
A single tab trigger (role=tab). Selected, hover, active, focus, and disabled states all live here.
panel
A content region (role=tabpanel) shown only when its tab is selected; focusable so keyboard users can scroll it.
overlay
The pseudo-element behind each tab that paints hover and active state tints.
Tokens & coverage
What the component consumes, checked live against what the algorithm produces.
Live coverage check against the xoji-default register
(derive(xojiDefault, { anchors }) →
coverComponent(manifest, register)). Every token this component
consumes must be a key the algorithm produces.
--accent
--accent-fg
--accent-text
--bg-1
--bg-2
--border-normal
--border-thick
--border-thin
--duration-fast
--ease-standard
--fg-0
--fg-1
--fg-2
--fg-disabled
--font-sans
--leading-tight
--line
--radius-lg
--radius-md
--radius-none
--radius-sm
--ring
--selection-cue
--space-0
--space-1
--space-2
--space-3
--space-4
--state-hover
--state-press
--text-body
--text-sm
--weight-medium
Slots
A tab trigger's label. Each slot="tab" element becomes a tab; its value attribute keys it and disabled marks it unselectable. (html / astro)
A tab's content. Each slot="panel" element is paired with the slot="tab" of the same order. In Svelte this is a panel snippet receiving the active value.
Accessibility
Code
Variants and activation
The same tablist semantics across the three treatments; declare tabs and panels as paired slotted elements (or, in Svelte, a tabs array plus a panel snippet).
<xoji-tabs variant="underline" label="Account settings" value="profile">
<button slot="tab" value="profile">Profile</button>
<div slot="panel">
<p>Your public profile information.</p>
</div>
<button slot="tab" value="billing">Billing</button>
<div slot="panel">
<p>Manage your subscription and payment methods.</p>
</div>
<button slot="tab" value="api" disabled>API</button>
<div slot="panel">
<p>API keys (coming soon).</p>
</div>
</xoji-tabs>
<script lang="ts">
import { Tabs } from "@xoji/svelte";
let active = $state("profile");
const tabs = [
{ value: "profile", label: "Profile" },
{ value: "billing", label: "Billing" },
{ value: "api", label: "API", disabled: true },
];
</script>
<Tabs {tabs} variant="pill" label="Account settings" bind:value={active}>
{#snippet panel(value)}
{#if value === "profile"}<p>Your public profile information.</p>{/if}
{#if value === "billing"}<p>Manage your subscription and payment methods.</p>{/if}
{#if value === "api"}<p>API keys (coming soon).</p>{/if}
{/snippet}
</Tabs>
---
import { Tabs } from "@xoji/astro";
---
<Tabs variant="enclosed" label="Account settings" value="profile">
<button slot="tab" value="profile">Profile</button>
<div slot="panel">
<p>Your public profile information.</p>
</div>
<button slot="tab" value="billing">Billing</button>
<div slot="panel">
<p>Manage your subscription and payment methods.</p>
</div>
</Tabs>