Navigation
The quietest surface in the product. Cloaked ships two mobile tab bars — an iOS bar with optional Liquid Glass, and an Android bar that follows Material 3. Same five destinations, two platform languages.
Playground
Drop into the real tab bar. Switch platforms, flip on Glass to see iOS 26's Liquid material, pick the active tab, add a notification dot. Everything here is rendered — not a mockup.
Usage
Reach for a bottom tab bar when the app has 3–5 top-level destinations and people will move between them often. It anchors the app's mental map at the bottom of the screen, within thumb reach.
Cloaked has exactly five tabs — Home, Exposure, Guard, Phone, Wallet — chosen because each represents a distinct mental model: triage, takedowns, call protection, masked numbers, and virtual cards. Every screen below the root anchors back to one of these five.
Types
Three platform variants, one mental model. Pick the one that matches the platform you're shipping for — never mix.
iOS · Liquid Glass
iOS 26+. Floating pill bar with translucent material that blurs the wallpaper underneath. Active tab gets a solid white pill.
iOS · Classic
iOS 17 and earlier. Opaque bar pinned to the safe area; selected tab uses a colored glyph and label, no background.
Android · Material 3
Material 3 navigation bar. Active tab gets a pill-shaped highlight behind the icon (the M3 "active indicator").
Web · Top bar
Desktop only. Horizontal nav with the brand on the left, primary destinations centered, account on the right.
Anatomy
Every tab bar is the same five parts, regardless of platform.
The bar itself — pinned to the bottom safe area on mobile, top of viewport on web.
24 × 24 SF Symbol (iOS) or M3 outlined glyph (Android). Outline default, filled when active.
Always visible on mobile. SF Pro 10/12 (iOS) or Roboto 12/16 (Android). Bold when active.
Optional 6×6 dot anchored to the icon's top-right. Uses Firey accent. Hides on the active tab.
iOS only. The system-drawn pill below the bar — leave 18 pt of padding for it.
States
A tab is in one of four states. Hover and pressed only fire on web; mobile tabs jump straight from default to active on tap.
Web patterns
On the dashboard and marketing site, navigation lives along the top or in a left sidebar. Bottom bars are mobile-only.
Sidebar
Dimensions
Heights and paddings for every tab bar variant.
| iOS Glass · bar height | 56 pt content + 38 pt safe-area padding |
|---|---|
| iOS Glass · pill width | 314 pt max, fluid on smaller screens |
| iOS Glass · radius | 9999 px (full pill) |
| iOS Classic · bar height | 49 pt content + 18 pt home indicator |
| Android M3 · bar height | 80 dp total · tab item 56 dp |
| Android M3 · indicator | 64 × 32 dp pill behind icon |
| Tab item width | 1fr · equal share of bar width |
| Hit target | 44 × 44 pt min (iOS HIG) · 48 × 48 dp (M3) |
Typography
Each platform uses its own type system for tab labels — never override.
| iOS · default | SF Pro Text · 500 · 10/12 |
|---|---|
| iOS · active | SF Pro Text · 700 · 10/12 |
| Android · default | Roboto · 500 · 12/16 · letter-spacing 0.5 |
| Android · active | Roboto · 500 · 12/16 · color shifts to --m3-on-surface |
| Web · top bar | SF Pro Display · 500 · 14/20 |
| Web · sidebar | SF Pro Display · 500 · 14/20 |
Color styles
Token-level color mapping for each state and surface.
iOS · Glass (light)
| Bar fill | rgba(255,255,255,0.55) + blur(26) + saturate(180%) |
|---|---|
| Bar border | rgba(255,255,255,0.6) |
| Active pill | rgba(255,255,255,0.95) |
| Default ink | --ink-default · #0F0F0F |
| Notification dot | --firey · #FF550C |
Android · Material 3 (light)
| Bar surface | --m3-surface · #F7F3EF |
|---|---|
| Active indicator | --m3-secondary-container · #E9DFC7 |
| Active label | --m3-on-surface · #1D1B20 |
| Inactive label | --m3-on-surface-variant · #49454F |
Spacing
Padding around the bar and between elements.
| iOS Glass · bar inset | 12 pt L/R · 38 pt bottom (home indicator + breathing room) |
|---|---|
| iOS Glass · pill padding | 6 pt vertical · 8 pt horizontal |
| iOS Classic · pad bottom | 18 pt for home indicator |
| Android M3 · pad bottom | 20 dp gesture inset |
| Tab gap | 0 on Android · 4 pt iOS Glass |
| Icon → label gap | 2 pt iOS · 4 dp Android |
Motion
Tab transitions are deliberately soft. The bar never moves; the active state animates between tabs.
| Active state transition | 180ms · cubic-bezier(0.16, 1, 0.3, 1) |
|---|---|
| Glass material | No transition — backdrop blur is real-time |
| Android indicator pill | 200ms ease-out — fades in behind icon, doesn't slide |
| Notification dot | 240ms spring — scale from 0 to 1 with subtle overshoot |
| Tab press | System default haptic on iOS · M3 ripple on Android |
Three to five tabs — never more
Each tab is a top-level mental model. With six or more, tabs stop being navigable destinations and become a list. If you need more, the seventh idea isn't a top-level destination.
Cap at five destinations. Each tab represents a distinct mental model — Home, Exposure, Guard, Phone, Wallet.
Don't pack seven tabs into the bar. The icons get cramped, labels truncate, and people stop scanning.
Never mix iOS and Android styles in one app
If you're shipping iOS, pick Glass or Classic. If you're shipping Android, use M3. Mixing platform languages in the same product makes it feel like a hybrid wrapper.
Use one platform language end-to-end. iOS users get iOS Glass; Android users get Material 3.
Don't drop a Material 3 bar into an iOS app. The pill highlight, label sizing, and ink colors all read as foreign.
Always show labels
Tab bars in Cloaked never run icon-only. Even when the icon is unambiguous, labels reduce the cognitive load of scanning and help first-time users build a mental map of the app.
Pair every glyph with a one-word label. Labels stay visible at every breakpoint.
Don't strip labels for "minimalism." Glyphs alone force users to memorize what each tab does.
The bar is always present at the root
The tab bar is the spine of the app. It should be visible on every root screen — Home, Exposure, Guard, Phone, Wallet — so users always have a way back.
Persist the bar across all root destinations. People always know how to get back home.
Don't hide the bar on root screens. (Hiding inside a flow — checkout, onboarding — is fine.)
Hide the bar inside flows, not on screens
When the user enters a focused, multi-step flow (signup, settings deep-dive, payment) hide the bar to remove distraction. Don't hide it on a regular detail screen — they need to be able to bail out.
Hide on a payment flow that has its own progress chrome and a clear cancel.
Don't hide on a detail screen. Pushing one level deep shouldn't strip the user's escape hatch.
Use the notification dot for new content, not counts
The dot says "something happened" — it doesn't say "five things happened." Save count badges for inbox-style screens (Exposure list) where the number meaningfully changes how the user reads the screen.
Show a dot when there's new, unseen activity in a tab. Removes after the user opens the tab.
Don't show a number badge in the tab bar. Counts belong on inbox rows, not on root navigation.
Accessibility
Tab bars need to work for keyboard, screen reader, and switch users — not only for thumbs.
- Each tab has
role="tab"andaria-selected; the bar hasrole="tablist". - Active tab announces as "selected" — VoiceOver, TalkBack, NVDA all read this from the role.
- Hit target ≥ 44 × 44 pt (iOS HIG) or 48 × 48 dp (M3) — already enforced by tab heights.
- Color contrast for default (inactive) ink: 4.5:1 against the bar surface, both light and dark.
- Labels translate. Never hardcode them inside the icon SVG.
- The notification dot has a redundant text equivalent in the accessibility label ("Exposure tab, 1 new alert").
React Native
The <TabBar> component handles platform detection — drop it into your root navigator and it picks Glass or Material 3 automatically.
// Root navigator <TabBar platform="auto" // "ios" | "android" | "auto" glass={true} // iOS only — opt-in to Liquid Glass active="home" onChange={(id) => navigate(id)} tabs={[ { id: 'home', label: 'Home', icon: 'house' }, { id: 'exposure', label: 'Exposure', icon: 'shield', notify: true }, { id: 'guard', label: 'Guard', icon: 'phone-shield' }, { id: 'phone', label: 'Phone', icon: 'phone' }, { id: 'wallet', label: 'Wallet', icon: 'card' }, ]} />
Web (HTML)
For the dashboard top bar, use a <nav> with role="tablist" and a single active link.
<nav class="top-nav" role="tablist"> <a role="tab" aria-selected="true" href="/">Home</a> <a role="tab" aria-selected="false" href="/exposure">Exposure</a> <a role="tab" aria-selected="false" href="/guard">Guard</a> </nav>
Props
platform— "ios" | "android" | "auto". Auto-detects viaPlatform.OS.glass— boolean. iOS only. Opts into iOS 26 Liquid Glass material.active— string. The id of the currently selected tab.onChange(id)— function. Fires when a tab is pressed. Returns the tab id.tabs— array. Each item:{ id, label, icon, notify? }.appearance— "light" | "dark" | "auto". Defaults to"auto".safeAreaInsets— { bottom: number }. Override if your nav controller doesn't provide them.onLongPress(id)— function. Optional secondary action (deep link, scroll-to-top).
CSS tokens
For the web nav, the same tokens used by buttons.css and forms.css apply.
/* Surface */ --nav-bg: var(--bg-canvas); --nav-border: var(--border-default); /* Item */ --nav-item-fg: var(--fg-subtle); --nav-item-fg-active: var(--fg-default); --nav-item-accent: var(--accent-primary);