Tabs
Horizontal navigation within a single context — primary tabs (top of content) and secondary tabs (filter within tab). Material parity (`/components/tabs`). Includes scrollable tabs, swipe gesture, and accessibility contract.
Spec — Tabs
Facet *isual*do Koder Design. Material parity: https://m3.material.io/components/tabs.
2 tab levels
| Level | Position | Use |
|---|---|---|
| *rimary* | Top of content area, beneath app bar | Main view switcher (Profile / Activity / Settings) |
| *econdary* | Within a primary tab's content | Filter within view (All / Mine / Shared) |
Maximum 2 levels — never nest a 3rd. If needed, use sub-navigation (drawer, menu) at the deeper level.
Anatomy (primary tab row)
┌────────────────────────────────────────────┐
│ Profile Activity Settings │ ← labels
│ ━━━━━━━ │ ← indicator (active)
└────────────────────────────────────────────┘- *ontainer* full width, surface bg
- *abel*
title-small(14/20, weight 600) - *ndicator* 2 px line below active tab,
accentcolor - *adding per tab* 16 px horizontal, 12 px vertical
- *eight* 48 px
R1 — Tab content options
| Mode | Content |
|---|---|
| Label only | Just text |
| Icon + Label | 18 px icon above OR before label |
| Icon only | Single 24 px icon (with aria-label) |
| Label + count | "Inbox (12)" — count as badge or inline |
Don't mix modes within a single tab row. All tabs in a row use the same mode.
R2 — States
| State | Label color | Indicator |
|---|---|---|
| Inactive (rest) | text-muted |
— |
| Inactive + hover | text |
— |
| Inactive + focused | text + focus ring |
— |
| Active (current) | accent |
accent line (2 px) |
| Active + focused | accent + focus ring |
accent line |
| Disabled | 38% opacity | — |
R3 — Indicator animation
When user clicks a different tab:
- Indicator slides from old position to new (motion-medium, ~250ms)
- Color of active label transitions from
text-muted→accent - Content area cross
fades to new content (motionfast) - Reduced motion: indicator snaps; no fade
R4 — Fixed vs scrollable tabs
| Mode | When | Behavior |
|---|---|---|
| *ixed* | Total tabs fit within container width | Equal-width tabs OR centered with auto sizing |
| *crollable* | Tabs overflow container | Horizontal scroll, no fixed width, indicator scrolls with tabs |
Switch automatically per layout: at Compact class scroll if more than 4 tabs; at Expanded class fixed up to 6 tabs.
R5 — Swipe gesture
On touch surfaces:
- Swipe left/right within content area switches to adjacent tab
- Swipe matches indicator motion (1:1 with finger)
- Release with > 50% threshold: switches; < 50%: snaps back
- Indicator animates smoothly with swipe
Disabled when content has its own horizontal scroll (e.g., carousel).
R6 — Tab content area
- Content area renders below tab row
- Switching tabs: cross
fade (motionfast) OR slidein (motionmedium)— pick one style per app; don't mix
- Preserve scroll position per tab (going back to "Profile" tab
remembers its scroll)
- Lazy-render content on first visit; keep mounted after
R7 — Secondary tabs
Look slightly different from primary to indicate hierarchy:
| Aspect | Primary | Secondary |
|---|---|---|
| Container | Same as toolbar | Same as surrounding content |
| Indicator | 2 px solid below | 2 px or chip-style border |
| Padding | 16 px horizontal | 12 px horizontal |
| Label weight | 600 | 500 |
Secondary tabs render WITHIN a primary tab's content area, not in the app bar.
R8 — Accessibility
role="tablist"on containerrole="tab"on each tab;aria-selected="true"on activearia-controlspointing to the panel id- Panel:
role="tabpanel"+aria-labelledbypointing to tab id - Keyboard:
- Arrow Left/Right cycles tabs (wrap around at ends)
- HomeEnd jumps to firstlast
- Tab moves OUT of tab row into the panel
- Screen reader announces: "Tab, Activity, 2 of 3, selected"
R9 — Per-preset variation
| Preset | Indicator |
|---|---|
material3 |
2 px solid line at bottom |
material2 |
3 px solid line at bottom |
ios_cupertino |
Active tab has accent-tint bg (no line) |
gnome |
Active tab background tint + 1 px line |
windows_11 |
Mica backdrop on row, accent line |
brutalist |
4 px thick line + sharp corners |
terminal_classic |
ASCII underline [==Active==] |
R10 — Forbidden patterns
- ❌ More than 2 levels of tabs (use drawer/menu for deeper nav)
- ❌ Tab labels longer than 20 chars (truncate or use shorter)
- ❌ Hiding inactive tabs (defeats the "all options visible" purpose)
- ❌ Confirmation dialog on tab switch (defeats quick navigation)
- ❌ Tabs that navigate to different URLs (use links, not tabs —
tabs are within ONE context)
Cross-link
interaction/states.kmd— focus/hover visualsthemes/color-roles.kmd— accent indicatorthemes/motion.kmd— indicator slide animationfoundations/elements.kmd— Control family