Bottom tabs
The primary phone-resolution nav for every portal — 3 to 5 thumb-reachable destinations at the bottom of the viewport. Replaces the nine bespoke .gd-tabs / .ow-tabs / .pa-tabs etc. shipped by individual portals.
Default
Four tabs, one active. Each tab is a stacked icon + label, ≥ 52 px tall so the touch target clears the iOS / Android guidance.
padding-bottom equal to tab-bar height + safe-area inset so the last row clears the bar.Parts
| Property | Value | Notes |
|---|---|---|
| height | 64 px + safe-area | Includes env(safe-area-inset-bottom) on iOS |
| min tap target | 52 × 52 px | iOS HIG floor; Android Material recommends 48 |
| tab count | 3–5 | 4 is the canonical sweet-spot; 5+ goes into a "More" sheet |
| active colour | --brand-strong (label), --brand (icon) | Inherits per-portal theme |
| inactive colour | --text-subtle | Hover lifts to --text-strong |
| badge | --tone-danger pill, top-right | For unread counts; suppress at 0 |
| elevation | 0 -6 18 px -8 ink @ 10% | Subtle lift off the canvas |
| visibility | ≤ 900 px viewport | Hidden ≥ 901 px where sidebar takes over |
Positioning
The contract for where this block lives in the DOM.
This block is position: fixed; left: 0; right: 0; bottom: 0. Place it as a direct child of <body> (or the demo phone-stage shell), never nested inside a scrolling container — the fixed positioning needs the viewport as its containing block, and a nested ancestor with transform or filter applied will silently turn the bar into position: absolute relative to that ancestor.
For doc-page previews that need to render the bar inside a 280-px stage frame, override locally with position: absolute on the stage child (as this page does). A roadmap modifier (.b-bottom-tabs--inline) is tracked under audit A-3 to ship this as a first-class variant.
Pad page content with padding-bottom: calc(64px + env(safe-area-inset-bottom)) so the bar never covers the last row.
"More" overflow
When the portal has 5+ destinations, the fifth tab becomes "More" and opens an x-bottom-sheet with the remaining items as a list. Never spill past 5 tabs in the bar itself.
Do & don't
Keep labels to 1 word. "Notes" not "Notifications". The bar must stay scannable in a thumb-flick.
Pack more than 5 tabs. Touch targets compress, labels truncate. Use a "More" sheet instead.
Pad page content with padding-bottom: calc(64px + env(safe-area-inset-bottom)) so the bar never covers the last row.
Show the bar on tablet / desktop. ≥ 901 px the app shell takes over — the bar would compete with the sidebar.
Carry unread badges. The bar is the operator's only signal at phone resolution that something needs attention elsewhere in the app.
Animate the active-state transition. The tap is the feedback; movement is noise.
Accessibility
- Wrap with
<nav aria-label="Primary">so screen readers announce the landmark. - Each tab is a
<button>(or<a>if it routes via URL). The active tab carriesaria-current="page". - The badge gets a visually-hidden context label:
<span class="sr-only">2 unread</span>. Pure numbers read as ambiguous out of context. - Tabs must be reachable by keyboard tab order even though the bar is phone-first — desktop users still hit it on narrow windows.