Skip to content

Tab bar

A tab bar is the primary navigation for a compact app. The default bar spans the width, with the icon above the label. af-is-fixed pins it to the bottom of the viewport, including the safe-area inset.

This is separate from af-tabs, which switches in-page panels. The previews below move aria-current="page" when you choose a tab, and a motion bar also updates the indicator position.

<nav class="af-tabbar" aria-label="Primary">
  <a class="af-tabbar-item" href="#home" aria-current="page">
    <span class="af-tabbar-icon" aria-hidden="true">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <path d="M4 10.5 12 4l8 6.5V20H4z"/>
      </svg>
    </span>
    <span class="af-tabbar-label">Home</span>
  </a>
  <a class="af-tabbar-item" href="#search">
    <span class="af-tabbar-icon" aria-hidden="true">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <circle cx="11" cy="11" r="6"/>
        <path d="m20 20-3.5-3.5"/>
      </svg>
    </span>
    <span class="af-tabbar-label">Search</span>
  </a>
  <a class="af-tabbar-item" href="#inbox">
    <span class="af-tabbar-icon" aria-hidden="true">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <path d="M4 6h16v12H4z"/>
      </svg>
      <span class="af-dot" role="img" aria-label="New notifications"></span>
    </span>
    <span class="af-tabbar-label">Inbox</span>
  </a>
</nav>

Mark the current link with aria-current="page". A notification af-dot sits on the icon with role="img" and an aria-label such as “New notifications”. The visible label and the dot name are both read with the link.

af-is-floating insets the bar, rounds it into a capsule, and shows the label only on the current item.

<nav class="af-tabbar af-is-floating" aria-label="Primary">
  <a class="af-tabbar-item" href="#home" aria-current="page">
    <span class="af-tabbar-icon" aria-hidden="true">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <path d="M4 10.5 12 4l8 6.5V20H4z"/>
      </svg>
    </span>
    <span class="af-tabbar-label">Home</span>
  </a>
  <a class="af-tabbar-item" href="#search">
    <span class="af-tabbar-icon" aria-hidden="true">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <circle cx="11" cy="11" r="6"/>
        <path d="m20 20-3.5-3.5"/>
      </svg>
    </span>
    <span class="af-tabbar-label">Search</span>
  </a>
  <a class="af-tabbar-item" href="#inbox">
    <span class="af-tabbar-icon" aria-hidden="true">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <path d="M4 6h16v12H4z"/>
      </svg>
    </span>
    <span class="af-tabbar-label">Inbox</span>
  </a>
</nav>

af-is-motion draws a highlight behind the current item and animates it when the custom properties change. Measure the current .af-tabbar-item and set these on the bar:

  • --af-tabbar-indicator-x — offsetLeft
  • --af-tabbar-indicator-width — offsetWidth

Include an .af-tabbar-indicator element. Without af-is-motion, the current item is styled in place and no measurement is required. prefers-reduced-motion: reduce drops the transition.

<nav
class="af-tabbar af-is-floating af-is-motion"
style="--af-tabbar-indicator-x: 0.35rem; --af-tabbar-indicator-width: 6.5rem;"
aria-label="Primary"
>
  <span class="af-tabbar-indicator" aria-hidden="true"></span>
  <a class="af-tabbar-item" href="#home" aria-current="page">
    <span class="af-tabbar-label">Home</span>
  </a>
  <a class="af-tabbar-item" href="#search">
    <span class="af-tabbar-label">Search</span>
  </a>
  <a class="af-tabbar-item" href="#inbox">
    <span class="af-tabbar-label">Inbox</span>
  </a>
</nav>