I9kTabs
Accessible horizontal tabs (the WAI-ARIA tabs pattern with automatic activation): a named tablist of equal-width tabs over the one panel that belongs to the selected tab.
ts
import { I9kTabs } from '@9klabs/design';Examples
Sign-in modes
vue
<I9kTabs v-model="mode" :tabs="modes" label="Sign in" :focusable-panel="false">
<template #login>
<I9kInput v-model="email" label="Email" type="email" />
</template>
<template #signup>
<I9kInput v-model="name" label="Full name" />
</template>
</I9kTabs>Small, with a disabled tab
Nine modules, from the first prompt to a shipped project.
vue
<I9kTabs v-model="section" :tabs="sections" label="Course details" size="sm">
<template #overview><p>Nine modules, from the first prompt to a shipped project.</p></template>
<template #curriculum><p>Each module ends with a hands-on exercise.</p></template>
</I9kTabs>Watch out
- Only the selected panel is rendered, so its content remounts with fresh state on every switch; keep state that must survive a switch in the parent.
labelis required: it names the tablist for screen readers.- Set
:focusable-panel="false"(bound, not a plain attribute) when the panel starts with a focusable field, so the panel is not an extra tab stop. - Arrow keys mirror in a right-to-left context: ArrowLeft moves to the next tab.
- Bind
v-model; a value naming no enabled tab shows the first enabled tab without emitting.
API
Props
| Prop | Type | Default | Required |
|---|---|---|---|
modelValue | string | — | yes |
tabs | I9kTabItem[] | — | yes |
label | string | — | yes |
size | 'sm' | 'md' | 'lg' | 'md' | no |
focusablePanel | boolean | true | no |
Emits
| Event | Payload |
|---|---|
update:modelValue | [value: string] |
Slots
[selectedTab.value]
Types
ts
export interface I9kTabItem {
/** Identifies the tab in `v-model` and names the slot that holds its panel content. */
value: string;
/** The tab's visible text and accessible name. */
label: string;
/** Skipped by the arrow keys and cannot be selected. */
disabled?: boolean;
}Agent prompt
Paste this into an agent to have it use the component correctly. It is self-contained.
text
Use I9kTabs from @9klabs/design to switch between a few alternative views in one place, such as "I have an account" and "New account" in a sign-in dialog.
import { I9kTabs } from '@9klabs/design';
import type { I9kTabItem } from '@9klabs/design';
Props:
- modelValue: string (required) — bind with v-model; the selected tab's `value`.
- tabs: I9kTabItem[] (required) — { value: string; label: string; disabled?: boolean }. `value` names the tab's slot; `label` is its visible text and accessible name; a disabled tab cannot be selected and the arrow keys skip it.
- label: string (required) — the tablist's accessible name (aria-label); translate it.
- size?: 'sm' | 'md' | 'lg' (default 'md') — the tab height, text size, and the gap above the panel.
- focusablePanel?: boolean (default true) — gives the panel tabindex="0" so keyboard users can reach content that holds nothing focusable. Set it to false when the panel starts with a form field.
Emits:
- update:modelValue(value: string) — the user selected another tab, by click or keyboard.
Slots:
- one named slot per tab `value`, e.g. <template #login="{ value }">. It receives { value } and holds that tab's panel content.
Behavior: tabs share the row width equally. Clicking a tab selects it. On a focused tab, ArrowRight and ArrowLeft move to the next and previous enabled tab, wrapping at both ends, and Home and End go to the first and last; moving selects the tab and focuses it. In a right-to-left context (the tab's computed direction is rtl) the arrows are mirrored, so ArrowLeft moves to the next tab. A modelValue that matches no enabled tab shows the first enabled tab, without emitting.
IMPORTANT: only the selected tab's panel is rendered, so its content unmounts when another tab is selected and mounts again, with fresh state, when it is selected again. Keep state that must survive a switch in the parent.
IMPORTANT: `label` is required because a tablist must have a name; do not repeat the tab labels in it.
IMPORTANT: bind v-model. Without an update:modelValue listener, defineModel keeps the selection locally and the parent never hears of it.
Usage:
<I9kTabs
v-model="mode"
:tabs="[
{ value: 'login', label: 'I have an account' },
{ value: 'signup', label: 'New account' },
]"
label="Sign in"
:focusable-panel="false"
>
<template #login><LoginForm /></template>
<template #signup><SignupForm /></template>
</I9kTabs>