Bottom Navigation

Mobile bottom navigation, also known as a dock, with semantic current-page states

Bottom Navigation (Dock)

Bottom navigation bars, often called docks, let users move between an app’s primary destinations. Use anchors for destinations and mark the current one with aria-current="page". The legacy active and bottom-nav-item-active classes remain supported for compatibility.

The component is fixed by default. Reserve at least its height plus env(safe-area-inset-bottom) as padding at the end of the application’s scrollable content so the dock does not cover the final controls.

Basic Usage

Basic Bottom Navigation

Active States

Primary Active (Default)

The default active state uses the secondary container color:

Primary Active State

Secondary Active

Use the secondary color scheme for active items:

Secondary Active State

Tertiary Active

Use the tertiary color scheme for active items:

Tertiary Active State

Numeric Badges

Show notification counts with numeric badges:

Numeric Badge

Dot Badges

Use dot badges for simple notification indicators:

Dot Badge

Display Modes

Labels Only Mode

Show only labels without icons for a text-based navigation:

Labels Only Mode

Shift Mode

In shift mode, labels only appear on the active item:

Shift Mode

Surface Variants

Default Surface

The default surface uses the surface-container color:

Default Surface

Surface

Use the base surface color:

Surface

Surface Container Low

Use a lower elevation surface:

Surface Container Low

Surface Container High

Use a higher elevation surface with shadow:

Surface Container High

Transparent

Create a transparent bottom navigation:

Transparent

Size Variants

Compact

A more compact navigation bar with reduced spacing:

Compact Size

Border Variants

Borderless

Remove the top border:

Borderless

States

Disabled Items

Unavailable destinations should not be links. Render a disabled button for an unavailable action, or plain text for a destination that cannot be visited:

Disabled Item

Best Practices

Bottom navigation is best for:

  • 3-5 top-level destinations
  • Mobile and tablet interfaces
  • Destinations of equal importance

Navigation Structure

Accessibility

Always ensure proper accessibility:

Accessibility Example

Label Guidelines

  • Keep labels short (1-2 words)
  • Use clear, unambiguous terms
  • Ensure labels are always visible or use shift mode

Label Guidelines

Home Search Profile User Profile Settings

Touch Targets

Navigation items are automatically sized for touch interfaces (minimum 64×48px):

Touch Targets

Framework Examples

React

interface BottomNavItemProps {
  icon: React.ReactNode;
  label: string;
  href: string;
  active?: boolean;
  badge?: string | number;
  disabled?: boolean;
  variant?: 'primary' | 'secondary' | 'tertiary';
}

function BottomNavItem({
  icon,
  label,
  href,
  active,
  badge,
  disabled,
  variant = 'primary'
}: BottomNavItemProps) {
  const activeClass = active
    ? variant === 'primary'
      ? 'bottom-nav-item-active'
      : `bottom-nav-item-active-${variant}`
    : '';

  const disabledClass = disabled ? 'bottom-nav-item-disabled' : '';

  return (
    <a
      href={href}
      className={`bottom-nav-item ${activeClass} ${disabledClass}`.trim()}
      aria-current={active ? 'page' : undefined}
    >
      <span className="bottom-nav-icon">{icon}</span>
      <span className="bottom-nav-label">{label}</span>
      {badge && (
        <span className={`bottom-nav-badge ${typeof badge === 'string' && badge === '' ? 'bottom-nav-badge-dot' : ''}`}>
          {badge}
        </span>
      )}
    </a>
  );
}

interface BottomNavigationProps {
  children: React.ReactNode;
  mode?: 'default' | 'shift' | 'labels-only';
  surface?: 'default' | 'surface' | 'surface-container-low' | 'surface-container-high' | 'transparent';
  compact?: boolean;
  borderless?: boolean;
}

function BottomNavigation({
  children,
  mode = 'default',
  surface = 'default',
  compact = false,
  borderless = false
}: BottomNavigationProps) {
  const modeClass = mode !== 'default' ? `bottom-nav-${mode}` : '';
  const surfaceClass = surface !== 'default' ? `bottom-nav-${surface}` : '';
  const compactClass = compact ? 'bottom-nav-compact' : '';
  const borderlessClass = borderless ? 'bottom-nav-borderless' : '';

  return (
    <nav
      className={`bottom-nav ${modeClass} ${surfaceClass} ${compactClass} ${borderlessClass}`.trim()}
      role="navigation"
      aria-label="Primary navigation"
    >
      {children}
    </nav>
  );
}

// Usage
<BottomNavigation mode="shift" surface="surface-container-high">
  <BottomNavItem
    href="#home"
    icon={<HomeIcon />}
    label="Home"
    active
  />
  <BottomNavItem
    href="#search"
    icon={<SearchIcon />}
    label="Search"
  />
  <BottomNavItem
    href="#notifications"
    icon={<BellIcon />}
    label="Notifications"
    badge={3}
  />
  <BottomNavItem
    href="#profile"
    icon={<UserIcon />}
    label="Profile"
  />
</BottomNavigation>

Vue

<template>
  <nav
    :class="navClasses"
    role="navigation"
    aria-label="Primary navigation"
  >
    <slot />
  </nav>
</template>

<script setup lang="ts">
import { computed } from 'vue'

interface Props {
  mode?: 'default' | 'shift' | 'labels-only'
  surface?: 'default' | 'surface' | 'surface-container-low' | 'surface-container-high' | 'transparent'
  compact?: boolean
  borderless?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  mode: 'default',
  surface: 'default',
  compact: false,
  borderless: false
})

const navClasses = computed(() => {
  return [
    'bottom-nav',
    props.mode !== 'default' && `bottom-nav-${props.mode}`,
    props.surface !== 'default' && `bottom-nav-${props.surface}`,
    props.compact && 'bottom-nav-compact',
    props.borderless && 'bottom-nav-borderless'
  ].filter(Boolean).join(' ')
})
</script>

<!-- BottomNavItem.vue -->
<template>
  <a
    :href="href"
    :class="itemClasses"
    :aria-current="active ? 'page' : undefined"
  >
    <span class="bottom-nav-icon">
      <slot name="icon" />
    </span>
    <span class="bottom-nav-label">{{ label }}</span>
    <span
      v-if="badge !== undefined"
      :class="badgeClasses"
      :aria-label="badgeLabel"
    >
      {{ badge }}
    </span>
  </a>
</template>

<script setup lang="ts">
import { computed } from 'vue'

interface Props {
  href: string
  label: string
  active?: boolean
  badge?: string | number
  disabled?: boolean
  variant?: 'primary' | 'secondary' | 'tertiary'
}

const props = withDefaults(defineProps<Props>(), {
  active: false,
  disabled: false,
  variant: 'primary'
})

const itemClasses = computed(() => {
  return [
    'bottom-nav-item',
    props.active && (
      props.variant === 'primary'
        ? 'bottom-nav-item-active'
        : `bottom-nav-item-active-${props.variant}`
    ),
    props.disabled && 'bottom-nav-item-disabled'
  ].filter(Boolean).join(' ')
})

const badgeClasses = computed(() => {
  return [
    'bottom-nav-badge',
    props.badge === '' && 'bottom-nav-badge-dot'
  ].filter(Boolean).join(' ')
})

const badgeLabel = computed(() => {
  if (typeof props.badge === 'number') {
    return `${props.badge} unread notifications`
  }
  return undefined
})
</script>

<!-- Usage -->
<BottomNavigation mode="shift" surface="surface-container-high">
  <BottomNavItem href="#home" label="Home" :active="true">
    <template #icon>
      <HomeIcon />
    </template>
  </BottomNavItem>

  <BottomNavItem href="#search" label="Search">
    <template #icon>
      <SearchIcon />
    </template>
  </BottomNavItem>

  <BottomNavItem href="#notifications" label="Notifications" :badge="3">
    <template #icon>
      <BellIcon />
    </template>
  </BottomNavItem>

  <BottomNavItem href="#profile" label="Profile">
    <template #icon>
      <UserIcon />
    </template>
  </BottomNavItem>
</BottomNavigation>

API Reference

Class Names

Class Description
.bottom-nav Base bottom navigation container (required)
.bottom-nav-item Individual navigation item (required)
.bottom-nav-item-active Active state with primary/secondary container color
.bottom-nav-item-active-primary Active state with primary/secondary container color (explicit)
.bottom-nav-item-active-secondary Active state with secondary container color
.bottom-nav-item-active-tertiary Active state with tertiary container color
.bottom-nav-item-disabled Disabled navigation item
.bottom-nav-icon Icon container
.bottom-nav-label Label text
.bottom-nav-badge Notification badge
.bottom-nav-badge-dot Dot-style badge indicator
.bottom-nav-labels-only Show only labels without icons
.bottom-nav-shift Labels only show on active item
.bottom-nav-shifting Animated shifting layout; preferred spelling for new code
.bottom-nav-surface Base surface background
.bottom-nav-surface-container-low Low elevation surface
.bottom-nav-surface-container-high High elevation surface with shadow
.bottom-nav-transparent Transparent background
.bottom-nav-compact Compact size variant
.bottom-nav-borderless Remove top border

Combinations

.bottom-nav-shift is retained as the legacy spelling. Prefer .bottom-nav-shifting for new animated shifting layouts.

You can combine classes for different effects:

Class Combinations

  • Button - Action buttons
  • Badge - Notification indicators
  • Card - Content containers

See Also