Bottom Navigation

Material Design 3 bottom navigation component for mobile-first navigation

Bottom Navigation

Bottom navigation bars allow users to navigate between primary destinations in an app. @duskmoon-dev/core provides a complete Material Design 3 bottom navigation implementation with support for icons, labels, badges, and multiple display modes.

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

Disable specific navigation items:

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

ClassDescription
.bottom-navBase bottom navigation container (required)
.bottom-nav-itemIndividual navigation item (required)
.bottom-nav-item-activeActive state with primary/secondary container color
.bottom-nav-item-active-primaryActive state with primary/secondary container color (explicit)
.bottom-nav-item-active-secondaryActive state with secondary container color
.bottom-nav-item-active-tertiaryActive state with tertiary container color
.bottom-nav-item-disabledDisabled navigation item
.bottom-nav-iconIcon container
.bottom-nav-labelLabel text
.bottom-nav-badgeNotification badge
.bottom-nav-badge-dotDot-style badge indicator
.bottom-nav-labels-onlyShow only labels without icons
.bottom-nav-shiftLabels only show on active item
.bottom-nav-surfaceBase surface background
.bottom-nav-surface-container-lowLow elevation surface
.bottom-nav-surface-container-highHigh elevation surface with shadow
.bottom-nav-transparentTransparent background
.bottom-nav-compactCompact size variant
.bottom-nav-borderlessRemove top border

Combinations

You can combine classes for different effects:

Class Combinations

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

See Also