Zetta v1.6

Components · Wayfinding

Tabs

Tab navigation component for switching between related views within the same context. Extracted from zetta-navigation-v1_4.md (removed in v1.5) in v1.4.

Preview

Your workspace overview and recent highlights.

Build with the skill

No package to install — hand this to your AI to generate or migrate the component in your stack.

Prompt for your AI
Apply the Zetta "Tabs" component to this project using the zetta-design-md skill.

WHEN TO USE
- Migrating or fixing existing code (the common case): audit the current implementation against the `tabs` spec and CORRECT every deviation — replace off-spec values, don't only add new ones.
- New component: build it to the `tabs` spec in this project's stack.

AUDIT & FIX (when the component already exists)
Compare it to the skill's spec and fix, don't just append: hardcoded colors/hex, off-scale radius, wrong fonts, dark/navy fills, shadows used in place of borders (or missing borders), brand lime used as a CTA or interaction (must be primary indigo), and any missing or wrong hover / focus / active / disabled / invalid state or missing 2px focus ring. Replace each with the correct Zetta token.

ALWAYS
- Pull the authoritative spec and tokens from the skill — never guess or hardcode values.
- Use this project's own component conventions and framework idioms.
- Honor the Zetta guardrails (brand = identity only, primary = interaction, shadows for overlays) and verify in Light, Dark, and Accessibility.

Anatomy & rules

┌────────────────────────────────────────────┐
│ [Tab 1]  [Tab 2 ●]  [Tab 3]  [Tab 4]      │  ← tabs-list (surface-tab-bg)
├────────────────────────────────────────────┤  ← hairline border
│                                            │
│  Tab panel content                         │  ← tabs-panel
│                                            │
└────────────────────────────────────────────┘

Parts:

  • tabs-list — container row for all tab triggers
  • tabs-trigger — individual clickable tab label
  • tabs-panel — content region revealed when tab is active
State Visual
Default {colors.muted} text, no border, transparent background
Hover {colors.ink} text, {colors.surface-muted} background
Focus 2px solid {colors.border-focus} inset outline
Active (selected) {colors.primary} text, 2px solid {colors.primary} bottom border, {colors.surface-tab-active} background
Disabled {colors.disabled-text} text, 50% opacity, not-allowed cursor

When to use

  • Switching between related views that share the same page context (e.g. Overview / Activity / Settings).
  • When all tab options should be visible simultaneously for easy comparison.

When not to use

  • For step-by-step flows — use a Stepper component.
  • For primary page navigation — use the Sidebar or Navigation bar.
  • When there are more than 7 tabs — consider a different navigation pattern.

Do

  • Keep tab labels short — 1–3 words. Use nouns, not verbs.
  • Always show a default active state — never render a tab list with no active tab.
  • Ensure the active tab is visually obvious: color AND border (not border alone).

Don't

  • Don't use tabs to show content that is not related to the same base context.
  • Don't mix tabs with breadcrumbs in the same header area.
  • Don't scroll tabs off-screen without a visual affordance (fade or scroll indicator).
  • Tab labels: nouns or noun phrases. "Overview", "Activity", "Members", "Settings".
  • Max 1–3 words per tab. If you need 4+ words, the label is too descriptive — simplify.
  • Never use verbs as tab labels. "View activity" → "Activity".
  • Active tab indicator: always both color AND border (not color alone — WCAG 1.4.1).
  • Tab count badge: use a Badge component inside the tab trigger — "Activity (3)".

Keyboard

Key Behavior
Tab Move focus to the tab list, then to the tab panel
Left Arrow Move focus to the previous tab trigger
Right Arrow Move focus to the next tab trigger
Home Focus the first tab trigger
End Focus the last tab trigger
Enter / Space Activate the focused tab

Focus moves between tab triggers via a roving tabindex — the active tab has tabindex="0", inactive tabs tabindex="-1".

Accessibility

  • ARIA role: role="tablist" on the list, role="tab" on each trigger, role="tabpanel" on each panel.
  • aria-selected="true" on the active tab trigger.
  • aria-controls="panel-id" on each trigger; aria-labelledby="tab-id" on each panel.
  • Tab panels must have outline: none with tabIndex={-1} to receive focus programmatically without showing a focus ring.
  • Color: active state uses both color ({colors.primary}) AND a 2px border — not color alone. Meets WCAG 1.4.1.
  • Contrast: {colors.muted} on {colors.surface-tab-bg} is 9.41:1 (AAA). {colors.primary} on {colors.surface-tab-active} is 6.06:1 (AA).

Spec

Values and token references resolved straight from the Zetta spec.

tabs-list
backgroundColor
{colors.surface-tab-bg}
borderBottom
1px solid {colors.hairline}
display
flex
flexDirection
row
gap
0
padding
0
overflowX
auto
tabs-trigger
backgroundColor
transparent
textColor
{colors.muted}
fontFamily
Geist
fontSize
14px
fontWeight
500
lineHeight
1.4
paddingX
{spacing.md}
paddingY
{spacing.sm}
borderBottom
2px solid transparent
cursor
pointer
whiteSpace
nowrap
transition
color {motion.duration-fast}, border-color {motion.duration-fast}
tabs-trigger-default
textColor
{colors.muted}
borderBottomColor
transparent
tabs-trigger-hover
textColor
{colors.ink}
backgroundColor
{colors.surface-muted}
borderBottomColor
transparent
tabs-trigger-focus
outline
2px solid {colors.border-focus}
outlineOffset
-2px
tabs-trigger-active
textColor
{colors.primary}
borderBottomColor
{colors.primary}
borderBottomWidth
2px
backgroundColor
{colors.surface-tab-active}
tabs-trigger-disabled
textColor
{colors.disabled-text}
cursor
not-allowed
opacity
0.5
tabs-panel
paddingTop
{spacing.md}
outline
none