Navigation
Horizontal tabs for switching between views within the same context. Uses a render-prop pattern so panel content is always fully controlled by the caller.
Preview
Project Alpha is a design system documentation site built with Next.js and Tailwind CSS. It defines tokens, components, and guidelines for the Sitka design language.
Anatomy
The tab bar sits flush to a bottom border. The active indicator is a 2px accent underline anchored to the bottom of each tab button. Badges use the accent-subtle token.
| Part | Element | Token |
|---|---|---|
| Tab bar | role=tablist div | border-b border |
| Tab button | role=tab button | text-tertiary → text-primary on active |
| Active indicator | span ::after | h-[2px] bg-accent rounded-t-full |
| Badge | span | bg-accent-subtle text-accent |
| Panel | role=tabpanel div | pt-4 |
Implementation
The children render prop receives the active tab id, making panel content fully flexible with no hidden DOM toggling.
import { Tabs } from "@/components/ui/Tabs";
<Tabs
tabs={[
{ id: "overview", label: "Overview" },
{ id: "versions", label: "Versions", badge: "3" },
{ id: "settings", label: "Settings" },
]}
>
{(active) => (
<div>
{active === "overview" && <p>Overview panel content.</p>}
{active === "versions" && <p>Versions panel content.</p>}
{active === "settings" && <p>Settings panel content.</p>}
</div>
)}
</Tabs>Props
| Prop | Type | Default | Description |
|---|---|---|---|
tabs | Tab[] | — | Array of { id, label, badge? } objects defining each tab. |
defaultTab | string | — | id of the tab active on first render. Defaults to the first tab. |
children | (activeId: string) => ReactNode | — | Render prop that receives the active tab id and returns the panel content. |
Mobile
Tab bars on mobile face overflow and touch-target challenges. Keep tabs to 5 or fewer and consider alternative navigation patterns for deeper hierarchies.
Repository
Project Alpha
v2.4.1 · Production
Last deploy 2h ago
3 members
| Scenario | Guidance |
|---|---|
| Overflow | More than 5 tabs will overflow on mobile. Use overflow-x: auto with scroll-snap for scrollable tabs, or collapse extras into an overflow menu. |
| Touch targets | Tab items need at least 44px height. Add extra vertical padding on mobile — py-3 instead of py-1.5. |
| Bottom tab bar | For app-like navigation, position the tab bar fixed at the bottom with padding-bottom: env(safe-area-inset-bottom). This matches iOS and Android conventions. |
| Icon + label | On bottom bars, pair each tab with an icon above the label. Icon-only tabs are harder to parse; label-only tabs miss the scanning affordance of icons. |
| Active indicator | Use a filled background or underline at least 3px tall. Thin indicators are hard to distinguish at arm's length on mobile. |
Accessibility
- →Tab buttons use role=tab and aria-selected. The container uses role=tablist.
- →Each panel uses role=tabpanel. Wire aria-controls and aria-labelledby for full ARIA compliance.
- →Keyboard: Left/Right arrows should cycle tabs. Tab key moves focus to the active panel. Implement with onKeyDown on the tablist.
- →Focus ring is visible only with keyboard navigation via focus-visible.
- →Badges are decorative counts — add aria-label to the tab button to include the count in the accessible name when relevant.