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.
A timeline of recent activity across the team.
Manage members, billing, and integrations.
Build with the skill
No package to install — hand this to your AI to generate or migrate the component in your stack.
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: nonewithtabIndex={-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