Tabs

mandatory

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, accent color
  • *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-mutedaccent
  • Content area crossfades 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: crossfade (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 container
  • role="tab" on each tab; aria-selected="true" on active
  • aria-controls pointing to the panel id
  • Panel: role="tabpanel" + aria-labelledby pointing 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)

  • interaction/states.kmd — focus/hover visuals
  • themes/color-roles.kmd — accent indicator
  • themes/motion.kmd — indicator slide animation
  • foundations/elements.kmd — Control family

Source: ../home/koder/dev/koder/meta/docs/stack/specs/components/tabs.kmd