Tab bar
The primary navigation surface for the mobile app. Fixed to the bottom edge, glass-blurred, iOS safe-area aware. Up to 5 slots — icon above mono label. Active tab teal; everything else muted. It stays out of the way until you need it.
Variants
The center slot — "add" — uses an elevated ink circle with a prominent plus. It breaks the flat tab pattern intentionally: adding an expense is the app's core action, always one tap away. The circle lifts 8px above the bar.
The "settle" tab has a coral dot badge — there are outstanding balances to resolve. Badges are always a dot, never a count number.
Sizes
| Slot count | Use case | Notes |
|---|---|---|
| 3 slots | MVP / tight information architecture | Always keep "add" in the center |
| 4 slots | When "groups" and "settle" need separation | Add center action moves to a floating button |
| 5 slots | Full navigation with home, groups, add, settle, profile | Maximum — never exceed 5 |
States
| State | Icon color | Label color | Notes |
|---|---|---|---|
| Active | --teal | --teal | Icon shifts 1px upward — subtle lift |
| Default / inactive | --ink-mute | --ink-mute | Visually recedes |
| Badged | --ink-mute | --ink-mute | 8px coral dot, upper-right of icon area |
| Disabled | 40% opacity | 40% opacity | Only for tabs gated behind onboarding or paywall |
On dark surfaces
The glass blur on dark surfaces reads against --dark-canvas. The center "add" button inverts to paper fill in dark mode — ink on dark is too low contrast. Active teal is unchanged — it works on both surfaces.
Anatomy
| Part | Element | Notes |
|---|---|---|
| Container | .tab-bar | Fixed, bottom 0, 64px height, glass backdrop. Add padding-bottom: env(safe-area-inset-bottom) for iOS home indicator. |
| Tab item | .tab-bar-item | Flex child, equal width (flex: 1). Use <button> for SPA, <a> for multi-page. |
| Icon wrap | .tab-bar-icon-wrap | Relative positioned — needed for badge positioning. |
| Icon | .tab-bar-icon | 24×24, stroked SVG. Color transitions on active/hover. |
| Label | .tab-bar-label | 10px JetBrains Mono, lowercase. Never title case, never sentence case. |
| Badge | .tab-bar-badge | 8px coral dot, absolute positioned inside icon-wrap. |
Usage
.active class and aria-current="page". Always include safe-area padding for iPhone.
Accessibility
<nav aria-label="Main navigation">. The active item gets aria-current="page". Icon-only scenarios (no label) require aria-label on each button.
Tab. Activated by Enter or Space. Focus ring: 2px solid var(--teal) inside the item bounds.
Code
HTML
<nav class="tab-bar" aria-label="Main navigation"> <button class="tab-bar-item"> <div class="tab-bar-icon-wrap"> <div class="tab-bar-icon"><!-- SVG --></div> </div> <span class="tab-bar-label">home</span> </button> <!-- Active tab --> <button class="tab-bar-item active" aria-current="page"> <div class="tab-bar-icon-wrap"> <div class="tab-bar-icon"><!-- SVG --></div> </div> <span class="tab-bar-label">groups</span> </button> <!-- Badged tab --> <button class="tab-bar-item"> <div class="tab-bar-icon-wrap"> <div class="tab-bar-icon"><!-- SVG --></div> <div class="tab-bar-badge" aria-label="Has pending settlements"></div> </div> <span class="tab-bar-label">settle</span> </button> </nav>
Design tokens used
| Token | Value | Role |
|---|---|---|
--glass-bg | rgba(251,248,240,0.72) | Tab bar surface |
--glass-border | rgba(255,255,255,0.55) | Top border |
--teal | #0E7C66 | Active icon + label |
--ink-mute | #8A8276 | Inactive icon + label |
--coral | #E76F51 | Badge dot |
--font-mono | JetBrains Mono | Tab labels |
--z-nav | 50 | Stack order |
--d-fast | 140ms | Color transitions |