Design System /Components /Navigation

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.

Home Exposure Guard

Anatomy

Every tab bar is the same five parts, regardless of platform.

Home
Exp.
Guard
Phone
Wallet
1
2
3
4
5
1Container

The bar itself — pinned to the bottom safe area on mobile, top of viewport on web.

2Icon

24 × 24 SF Symbol (iOS) or M3 outlined glyph (Android). Outline default, filled when active.

3Label

Always visible on mobile. SF Pro 10/12 (iOS) or Roboto 12/16 (Android). Bold when active.

4Notification dot

Optional 6×6 dot anchored to the icon's top-right. Uses Firey accent. Hides on the active tab.

5Home indicator

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.

DEFAULT · INACTIVE
ACTIVE
HOVER (WEB)
DISABLED

Web patterns

On the dashboard and marketing site, navigation lives along the top or in a left sidebar. Bottom bars are mobile-only.

Sidebar

Dashboard
Triage exposures, monitor takedowns, manage your protected identities.

Dimensions

Heights and paddings for every tab bar variant.

iOS Glass · bar height56 pt content + 38 pt safe-area padding
iOS Glass · pill width314 pt max, fluid on smaller screens
iOS Glass · radius9999 px (full pill)
iOS Classic · bar height49 pt content + 18 pt home indicator
Android M3 · bar height80 dp total · tab item 56 dp
Android M3 · indicator64 × 32 dp pill behind icon
Tab item width1fr · equal share of bar width
Hit target44 × 44 pt min (iOS HIG) · 48 × 48 dp (M3)

Typography

Each platform uses its own type system for tab labels — never override.

iOS · defaultSF Pro Text · 500 · 10/12
iOS · activeSF Pro Text · 700 · 10/12
Android · defaultRoboto · 500 · 12/16 · letter-spacing 0.5
Android · activeRoboto · 500 · 12/16 · color shifts to --m3-on-surface
Web · top barSF Pro Display · 500 · 14/20
Web · sidebarSF Pro Display · 500 · 14/20

Color styles

Token-level color mapping for each state and surface.

iOS · Glass (light)

Bar fillrgba(255,255,255,0.55) + blur(26) + saturate(180%)
Bar borderrgba(255,255,255,0.6)
Active pillrgba(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 inset12 pt L/R · 38 pt bottom (home indicator + breathing room)
iOS Glass · pill padding6 pt vertical · 8 pt horizontal
iOS Classic · pad bottom18 pt for home indicator
Android M3 · pad bottom20 dp gesture inset
Tab gap0 on Android · 4 pt iOS Glass
Icon → label gap2 pt iOS · 4 dp Android

Motion

Tab transitions are deliberately soft. The bar never moves; the active state animates between tabs.

Active state transition180ms · cubic-bezier(0.16, 1, 0.3, 1)
Glass materialNo transition — backdrop blur is real-time
Android indicator pill200ms ease-out — fades in behind icon, doesn't slide
Notification dot240ms spring — scale from 0 to 1 with subtle overshoot
Tab pressSystem 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.

Do

Cap at five destinations. Each tab represents a distinct mental model — Home, Exposure, Guard, Phone, Wallet.

Don't

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.

Single platform
Do

Use one platform language end-to-end. iOS users get iOS Glass; Android users get Material 3.

iOS shell, M3 bar
Don't

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.

HOME · EXP · GUARD · PHONE · WALLET
Do

Pair every glyph with a one-word label. Labels stay visible at every breakpoint.

Don't

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.

Do

Persist the bar across all root destinations. People always know how to get back home.

No bar visible
Don't

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.

Add Card · Step 2 of 3
Do

Hide on a payment flow that has its own progress chrome and a clear cancel.

Exposure detail screen
Don't

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.

Do

Show a dot when there's new, unseen activity in a tab. Removes after the user opens the tab.

12
Don't

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" and aria-selected; the bar has role="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 via Platform.OS.
  • glassboolean. iOS only. Opts into iOS 26 Liquid Glass material.
  • activestring. The id of the currently selected tab.
  • onChange(id)function. Fires when a tab is pressed. Returns the tab id.
  • tabsarray. 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);