Switch between panels of related content in the same space.

Last updated

Uses JavaScript

Wires the WAI-ARIA tablist pattern: roving tabindex, arrow / Home / End keys, panel visibility (only the selected panel shows), edge fades when the list overflows, and scrolls the chosen tab into view.

Without JS: Every panel renders stacked and reachable — no content is hidden, so nothing is lost.

Manage your account details.
Change your password.
Invite and manage members.
Show code
<Tabs tabs={['Account', 'Password', 'Team']}>
<div slot="panel-0">Manage your account details.</div>
<div slot="panel-1">Change your password.</div>
<div slot="panel-2">Invite and manage members.</div>
</Tabs>

Add

npx @ava-ui/apogee add tabs

No Apogee dependencies. Ships one scoped JS island (progressive enhancement).

Usage

---
import Tabs from '@/components/apogee/Tabs/Tabs.astro'
---
<Tabs tabs={['Account', 'Password']}>
  <div slot="panel-0">Account settings…</div>
  <div slot="panel-1">Password settings…</div>
</Tabs>

Labels come from the tabs array; each panel goes in a matching panel-<i> slot (0-indexed, same order as the labels).

Anatomy

  • Tablist — the row of role="tab" buttons (the labels).
  • Tabpanel — one role="tabpanel" per tab; the first is shown, the rest are hidden until selected.

API

@composable — exposed to Content Editors in AVA. Any other attribute passes through to the root <div>.

Prop Type Default Description
tabs (string | { label, value?, href?, disabled?, icon?, badge? })[] — Tabs in order. A string is a label; an object adds a value (panel slot name and event value, defaults to the index), an href (renders a link, no panel), disabled, an icon (Lucide component) or a badge (count or short text, shown from md).
value string first enabled tab Value of the tab shown first.
variant 'bordered' | 'borderless' | 'pills' | 'pills-raised' | 'primary' | 'feature' 'bordered' Underline with a list border, underline alone, chip, chip in a raised tray, a primary-tinted chip, or a feature tab (title and line over a thick bar).
orientation 'horizontal' | 'vertical' 'horizontal' vertical stacks the list beside the panels; Up and Down move between tabs; underline variants draw on the start edge.
fullWidth boolean false Tabs share the list width equally (horizontal only).
activation 'automatic' | 'manual' 'automatic' manual: arrows move focus, Enter or Space selects. Use it when a panel is expensive to show.
label string 'Sections' Name of the <nav> when every tab is a link.
size 'small' | 'medium' | 'large' 'medium' Tab height and text size.
class string — Extra classes on the root, merged last.

Panels are slots named panel-<value>: panel-0, panel-1, … for string tabs, panel-billing for { value: 'billing' }.

Variants

Every variant:

bordered: account details.
bordered: password.
bordered: members.
borderless: account details.
borderless: password.
borderless: members.
pills: account details.
pills: password.
pills: members.
pills-raised: account details.
pills-raised: password.
pills-raised: members.
primary: account details.
primary: password.
primary: members.
Show code
<Tabs variant="bordered" tabs={['Account', 'Password', 'Team']}>…</Tabs>
<Tabs variant="borderless" tabs={['Account', 'Password', 'Team']}>…</Tabs>
<Tabs variant="pills" tabs={['Account', 'Password', 'Team']}>…</Tabs>
<Tabs variant="pills-raised" tabs={['Account', 'Password', 'Team']}>…</Tabs>
<Tabs variant="primary" tabs={['Account', 'Password', 'Team']}>…</Tabs>

Feature tabs

variant="feature" makes each tab a title and a description over a thick bar, for feature sections; vertical feature tabs put the bar on the start edge. The title names the tab and the line describes it. Put the panels above the row with flex flex-col-reverse on the root.

The overview panel.
The customers panel.
The settings panel.
The overview panel.
The customers panel.
Show code
<Tabs variant="feature" class="flex flex-col-reverse gap-8" tabs={[
{ label: 'Track what matters', description: 'Revenue, users and churn at a glance.' },
{ label: 'Know your customers', description: 'Every account, plan and seat in one table.' },
{ label: 'Set it up your way', description: 'Profile and preferences in one place.' },
]}>…</Tabs>
<Tabs variant="feature" orientation="vertical" tabs={[…]}>…</Tabs>

Sizes

small tabs.
Invoices.
medium tabs.
Invoices.
large tabs.
Invoices.
Show code
<Tabs size="small" tabs={['Overview', 'Billing']}>…</Tabs>
<Tabs size="medium" tabs={['Overview', 'Billing']}>…</Tabs>
<Tabs size="large" tabs={['Overview', 'Billing']}>…</Tabs>

Items

Objects add a value, a link (href), disabled or a description (feature tabs); value on the root picks the tab shown first.

Help
Overview of the account.
Invoices and payment method.
Show code
<Tabs
tabs={[{ label: 'Overview' }, { label: 'Billing', value: 'billing' }, { label: 'Archived', disabled: true }, { label: 'Help', href: '/docs/tabs#accessibility' }]}
value="billing"
>
<div slot="panel-0">Overview of the account.</div>
<div slot="panel-billing">Invoices and payment method.</div>
</Tabs>

Icons and badges:

Profile.
Billing.
Four members.
Twelve alerts.
Show code
<Tabs tabs={[{ label: 'Profile', icon: User }, { label: 'Billing', icon: CreditCard }, { label: 'Team', icon: Users, badge: 4 }, { label: 'Alerts', icon: Bell, badge: 12 }]}>…</Tabs>

Orientation

vertical puts the list beside the panels, for settings pages:

General settings.
Password and two-factor sign-in.
Email and push preferences.
Show code
<Tabs orientation="vertical" tabs={['General', 'Security', 'Notifications']}>…</Tabs>

Full width

Billed every month.
Two months free.
Pay once.
Show code
<Tabs fullWidth variant="pills-raised" tabs={['Monthly', 'Yearly']}>…</Tabs>

Activation

manual: arrows move focus without switching panels; Enter or Space selects.

Summary.
A heavy report.
Raw rows.
Show code
<Tabs activation="manual" tabs={['Summary', 'Report', 'Raw data']}>…</Tabs>

Overflow

The list scrolls, the hidden edge fades, and the selected tab is scrolled into view.

Overview panel.
Analytics panel.
Reports panel.
Notifications panel.
Integrations panel.
Billing panel.
Security panel.
Members panel.
Audit log panel.
Exports panel.
Show code
<Tabs class="max-w-sm" tabs={['Overview', 'Analytics', 'Reports', 'Notifications', 'Integrations', 'Billing', 'Security', 'Members', 'Audit log', 'Exports']}>
<div slot="panel-0">…</div>
…
</Tabs>

The tab that starts active is not scrolled into view on load. Put it among the first visible tabs, or call scrollIntoView on it.

States

  • Selected — the active tab has an accent underline and aria-selected="true"; its panel is visible.
  • Hover / focus — inactive tabs lighten on hover; keyboard focus shows a visible ring (focus-visible:outline-focus).
  • Disabled — dimmed, not clickable, skipped by the arrows.
  • No JS — every panel is shown (the no-js:block fallback).

Examples

Every tab a link (category pages, a blog filter): the row becomes a <nav> named by label, and value marks the current page with aria-current="page". Tab moves through the links; the arrow keys are left alone.

Show code
<Tabs label="Categories" value="design" tabs={[
{ label: 'View all', value: 'all', href: '/blog' },
{ label: 'Design', value: 'design', href: '/blog/design' },
{ label: 'Product', value: 'product', href: '/blog/product' },
]} />

More than a handful of tabs — they wrap; the pattern is unchanged:

At-a-glance summary.
Charts and trends.
Exportable reports.
Alert preferences.
Show code
<Tabs tabs={['Overview', 'Analytics', 'Reports', 'Notifications']}>
<div slot="panel-0">At-a-glance summary.</div>
<div slot="panel-1">Charts and trends.</div>
<div slot="panel-2">Exportable reports.</div>
<div slot="panel-3">Alert preferences.</div>
</Tabs>

Events

The root dispatches apogee:tabs-change (detail: { index, value }, bubbling) whenever a tab is selected.

Accessibility

Key Action
Tab Moves focus into the tablist (one stop), then to the active panel.
← → Move between tabs and select, wrapping at the ends; swapped under dir="rtl".
↑ ↓ With orientation="vertical", move between tabs instead of ← →.
Home End First / last tab.
Enter Space With activation="manual", select the focused tab (arrows then only move focus).
  • Full WAI-ARIA tabs pattern: role="tablist" / role="tab" / role="tabpanel", wired with aria-selected, aria-controls, and aria-labelledby.
  • A feature tab is named by its title (aria-labelledby) and described by its line (aria-describedby).
  • Each panel is focusable (tabindex="0"), so Tab from the active tab lands on its panel before moving on.
  • Focus — a visible focus-visible ring on each tab.
  • No JS — panels are never trapped: with scripts off every panel renders, so the content is fully reachable.
  • The panel enter animation respects prefers-reduced-motion.

Tokens

Token Used for
--border-neutral Tablist bottom rule + inactive tab border
--border-primary Selected tab underline
--foreground-neutral / --foreground-neutral-faded Active vs. inactive label colour
--outline-color-focus Focus ring
--duration-medium / --ease-standard Panel enter animation

Do / Don’t

  • Do keep labels to one or two words so the tablist stays scannable.
  • Do use tabs for parallel, peer-level content — not for a sequence (use Steps) or for progressive disclosure of one thing (use Accordion).
  • Don’t put a form that must submit as a whole across multiple tabs — a user can’t see all fields at once.
  • Don’t hide critical content behind a non-default tab if a no-JS user must act on it immediately (though it stays reachable, it’s below the fold).

Content

  • Labels are nouns, sentence case, one or two words (“Account”, “Billing history”).
  • No ending punctuation. Don’t number them — the order is the sequence.