Navigation

v0.0.4
Navigation

Tab bar

The main destinations of a small-screen app, along the bottom edge.

On this page

A tab bar holds the three to five main destinations of a small-screen app, along the bottom edge where the thumb rests. Each item is an icon over a short label. The current one is in ink and its icon rests on a soft pill; nothing else in the bar has a hue. The bar is a pane of frosted glass with a hairline above it, and it keeps clear of the home indicator.

Tab bar

<div class="well h(320) max-w(380) mx-auto overflow-y-auto">  <div class="grid gap-sm p-md">    <div class="sheet p-lg text-sm">Today</div>    <div class="sheet p-lg text-sm">Continue reading</div>    <div class="sheet p-lg text-sm">New this week</div>    <div class="sheet p-lg text-sm">From your library</div>  </div>  <nav class="tabbar" aria-label="Main">    <a class="tabbar-item" href="#" aria-current="page">      <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">        <path d="M4 11l8-7 8 7v8a1 1 0 0 1-1 1h-4v-6h-6v6H5a1 1 0 0 1-1-1z" />      </svg>      <span class="tabbar-label">Home</span>    </a>    <a class="tabbar-item" href="#">      <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">        <circle cx="11" cy="11" r="6" />        <path d="m20 20-4-4" />      </svg>      <span class="tabbar-label">Search</span>    </a>    <a class="tabbar-item" href="#">      <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">        <path d="M5 5h14v14H5zM9 9h6M9 13h6" />      </svg>      <span class="tabbar-label">Library</span>    </a>    <a class="tabbar-item" href="#">      <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">        <circle cx="12" cy="9" r="3.5" />        <path d="M5 20c1-4 4-5 7-5s6 1 7 5" />      </svg>      <span class="tabbar-label">Profile</span>    </a>  </nav></div>
Scroll the preview: content passes under the bar.

The bar is position: sticky at the bottom of its scroll container. In an app shell that does not scroll as a whole, put it after the scrolling region instead; it draws the same.

The tab bar is the bar itself. Where it sits among the regions of a page is a layout's decision: in a page it follows the main region, and in a fixed shell it takes the footer.

#Floating

tabbar-floating lifts the bar off the edge with a margin and a surface's corner, over a floating shadow.

Floating

<div class="tray h(320) max-w(380) mx-auto p(0) overflow-y-auto">  <div class="grid gap-sm p-md">    <div class="sheet p-lg text-sm">Today</div>    <div class="sheet p-lg text-sm">Continue reading</div>    <div class="sheet p-lg text-sm">New this week</div>    <div class="sheet p-lg text-sm">From your library</div>  </div>  <nav class="tabbar tabbar-floating" aria-label="Main">    <a class="tabbar-item" href="#">      <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">        <path d="M4 11l8-7 8 7v8a1 1 0 0 1-1 1h-4v-6h-6v6H5a1 1 0 0 1-1-1z" />      </svg>      <span class="tabbar-label">Home</span>    </a>    <a class="tabbar-item" href="#" aria-current="page">      <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">        <circle cx="11" cy="11" r="6" />        <path d="m20 20-4-4" />      </svg>      <span class="tabbar-label">Search</span>    </a>    <a class="tabbar-item" href="#">      <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">        <path d="M5 5h14v14H5zM9 9h6M9 13h6" />      </svg>      <span class="tabbar-label">Library</span>    </a>    <a class="tabbar-item" href="#">      <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">        <circle cx="12" cy="9" r="3.5" />        <path d="M5 20c1-4 4-5 7-5s6 1 7 5" />      </svg>      <span class="tabbar-label">Profile</span>    </a>  </nav></div>

#When to use

Use a tab bar for destinations of equal weight that a reader moves between all day. For views of one page use tabs; on a wide screen the same destinations belong in a navbar or a nav. Keep labels to one word: a label that does not fit is cut with an ellipsis before it pushes its neighbors.

#Accessibility

  • The bar is a <nav> with a label, and each item a link with aria-current="page" on the current one. If the items switch panels in place, use role="tablist" with role="tab" and aria-selected; both states draw the same.
  • Every item keeps its label. An icon alone is a guess; the word under it is the name.
  • Items are at least 56px tall and share the width equally, well over the 44px a touch target needs.
  • The bar adds env(safe-area-inset-bottom) under its items. Set viewport-fit=cover in the viewport meta tag for the inset to apply.

#Classes

ClassDescription
tabbarThe bar, sticky at the bottom, on a nav with a label: a pane of frosted glass whose items share its width equally
tabbar-floatingA bar lifted off the edge, with a margin, a surface corner and a floating shadow. With tabbar
tabbar-itemA destination: an icon over a tabbar-label, on a link or a button
tabbar-labelThe word under the icon, on one line, cut with an ellipsis before it pushes its neighbors

#Custom properties

PropertyDefaultDescription
--tabbar-height3.5remAn item's minimum height
--tabbar-colorvar(--color-surface-base)The color the pane is tinted with

#Source

@master/ui/components/tabbar.css
/* * Tab bar: the few main destinations of a small-screen app, along the bottom edge under the thumb. * Each item is an icon over a short label. The current one is in ink and its icon rests on a soft pill. * The bar is a pane of frosted glass with a hairline above it, and it keeps clear of the home indicator. */@layer components {  .tabbar {    --tabbar-height: 3.5rem;    --tabbar-color: var(--color-surface-base);    position: sticky;    bottom: 0;    z-index: 20;    display: grid;    grid-auto-columns: minmax(0, 1fr);    grid-auto-flow: column;    min-width: 0;    margin: 0;    padding: 0 .25rem env(safe-area-inset-bottom, 0px);    background: color-mix(in oklab, var(--tabbar-color) 78%, transparent);    box-shadow: inset 0 .5px 0 var(--color-line-strong);    backdrop-filter: blur(16px) saturate(1.7);    list-style: none;    /* Floating: a bar lifted off the edge, with the corner of a surface. */    &.tabbar-floating {      --tabbar-color: var(--color-surface-floating);      bottom: max(.75rem, env(safe-area-inset-bottom, 0px));      margin: .75rem;      padding: 0 .25rem;      border-radius: calc(var(--radius-surface) * var(--corner-scale) + 4px);      corner-shape: var(--corner-shape);      box-shadow: var(--shadow-floating);    }    @media (prefers-reduced-transparency: reduce) {      background: var(--tabbar-color);      backdrop-filter: none;    }    @media (forced-colors: active) {      border-top: 1px solid CanvasText;      background: Canvas;    }  }  .tabbar-item {    --icon-size: 1.25rem;    position: relative;    display: flex;    flex-direction: column;    align-items: center;    justify-content: center;    gap: .25rem;    min-width: 0;    min-height: var(--tabbar-height);    margin: 0;    padding: .375rem .25rem;    border: 0;    background: transparent;    color: var(--color-text-muted);    font: var(--font-weight-control) .6875rem/1 var(--font-family-sans);    font-feature-settings: inherit;    letter-spacing: .005em;    text-decoration: none;    cursor: pointer;    touch-action: manipulation;    isolation: isolate;    -webkit-tap-highlight-color: transparent;    transition: color var(--duration-quick) var(--easing-glide);    user-select: none;    -webkit-user-select: none;    /* The pill behind the icon: 56 by 28, centered on the icon's row. It grows in from a little smaller. */    &::before {      content: '';      position: absolute;      top: calc(50% - .5rem - 14px);      left: calc(50% - 1.75rem);      z-index: -1;      width: 3.5rem;      height: 1.75rem;      border-radius: 999px;      background: var(--color-surface-chip);      box-shadow: inset 0 0 0 .5px var(--color-line-hair);      opacity: 0;      scale: .8 .9;      transition-property: opacity, scale;      transition-duration: var(--duration-base);      transition-timing-function: var(--easing-glide);    }    @media (hover: hover) {      &:hover {        color: var(--color-text-strong);      }    }    &:is([aria-current="page"], [aria-selected="true"]) {      color: var(--color-text-strong);      &::before {        opacity: 1;        scale: 1;      }    }    &:active::before {      opacity: 1;      scale: .94;      transition-duration: var(--duration-press);    }    &:focus-visible {      @apply --focus-ring;      outline-offset: -4px;      border-radius: calc(var(--radius-control) * var(--corner-scale));      corner-shape: var(--corner-shape);    }    @media (prefers-reduced-motion: reduce) {      &::before {        scale: none;      }    }    @media (forced-colors: active) {      &:is([aria-current="page"], [aria-selected="true"]) {        color: Highlight;      }    }    &:is(:disabled, [aria-disabled="true"]) {      color: var(--color-text-disabled);      cursor: not-allowed;    }  }  /* The word under the icon: one line, cut with an ellipsis before it pushes its neighbors. */  .tabbar-label {    overflow: hidden;    max-width: 100%;    text-overflow: ellipsis;    white-space: nowrap;  }}