Navigation

v0.0.4
Navigation

Navbar

The bar at the top of a page, which takes its material once content scrolls under it.

On this page

A navbar is the bar at the top of a page or an app: the brand, the main links and a few actions. At the top of the page it has no material at all, so the page starts as one surface. Once content scrolls under it, the bar becomes a pane of frosted glass with a hairline and a short shadow. A scroll-state container query knows when the sticky bar is stuck; no script watches the scroll.

Navbar

<div class="h(280) overflow-y-auto">  <header class="navbar">    <a class="navbar-brand" 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="12" r="8" />        <path d="M12 4a8 8 0 0 0 0 16" />      </svg>Atlas</a>    <nav class="navbar-nav" aria-label="Main">      <a class="navbar-link" href="#" aria-current="page">Overview</a>      <a class="navbar-link" href="#">Projects</a>      <a class="navbar-link" href="#">Docs</a>    </nav>    <div class="navbar-end">      <button class="btn btn-quiet btn-square btn-sm navbar-toggle" aria-label="Open the menu">        <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 8h16M4 16h16" />        </svg>      </button>      <button class="btn btn-sm">Sign in</button>      <button class="btn btn-primary btn-sm">Get started</button>    </div>  </header>  <div class="grid gap-sm p-md">    <div class="sheet p-lg text-sm">Scroll this box.</div>    <div class="sheet p-lg text-sm">The bar has no material at the top.</div>    <div class="sheet p-lg text-sm">Once content is under it, it becomes a pane.</div>    <div class="sheet p-lg text-sm">A hairline and a short shadow part it from the page.</div>    <div class="sheet p-lg text-sm">Scroll back and it lets go.</div>  </div></div>
Scroll the preview: the pane appears when the bar sticks.

The bar is position: sticky at the top of its scroll container, which is the page in most layouts. Where the browser has no scroll-state queries, the pane is always drawn.

The navbar is the bar itself: its material, its stuck state and the width at which its links step aside. What goes around it is not its business. For a whole site header, with a menu, a second action and a drawer, copy the header block; to place the bar in a page with a sidebar, see the app shell layout.

#Narrow bars

The bar is also a size container. Below 36rem of bar, navbar-nav steps aside and navbar-toggle appears; at 36rem and above it is the other way round. Point the toggle at a drawer or a menu that holds the same links.

Narrow

<div class="max-w(380) mx-auto">  <header class="navbar">    <a class="navbar-brand" href="#">Atlas</a>    <nav class="navbar-nav" aria-label="Main">      <a class="navbar-link" href="#" aria-current="page">Overview</a>      <a class="navbar-link" href="#">Projects</a>    </nav>    <div class="navbar-end">      <button class="btn btn-primary btn-sm">Get started</button>      <button class="btn btn-quiet btn-square btn-sm navbar-toggle" popovertarget="navbar-menu" aria-label="Open the menu">        <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 8h16M4 16h16" />        </svg>      </button>      <div class="menu menu-end" id="navbar-menu" popover>        <a class="menu-item" href="#">Overview</a>        <a class="menu-item" href="#">Projects</a>        <a class="menu-item" href="#">Deployments</a>        <a class="menu-item" href="#">Docs</a>      </div>    </div>  </header></div>

#Floating

navbar-floating lifts the bar off the edge: a margin around it, the corner of a surface and its material from the start. Use it over a page with a strong first screen.

Floating

<div class="h(260) overflow-y-auto bg-surface-tray">  <header class="navbar navbar-floating">    <a class="navbar-brand ml-xs" href="#">Atlas</a>    <nav class="navbar-nav" aria-label="Main">      <a class="navbar-link" href="#" aria-current="page">Overview</a>      <a class="navbar-link" href="#">Projects</a>      <a class="navbar-link" href="#">Docs</a>    </nav>    <div class="navbar-end">      <button class="btn btn-primary btn-sm">Get started</button>    </div>  </header>  <div class="grid gap-sm p-md">    <div class="sheet p-lg text-sm">Scroll this box.</div>    <div class="sheet p-lg text-sm">The bar has no material at the top.</div>    <div class="sheet p-lg text-sm">Once content is under it, it becomes a pane.</div>    <div class="sheet p-lg text-sm">A hairline and a short shadow part it from the page.</div>    <div class="sheet p-lg text-sm">Scroll back and it lets go.</div>  </div></div>

#Reading progress

Put a navbar-progress inside the bar and a hairline of ink along its foot fills as the page scrolls. It is driven by a scroll timeline on the root scroller, so it shows the progress of the page, and it is drawn only where the browser supports scroll-driven animations.

<header class="navbar">  …  <div class="navbar-progress" role="presentation"></div></header>

#Accessibility

  • Use a <header> for the bar and a <nav> with a label for the links. Mark the current page with aria-current="page": it is the only thing that puts a link in ink.
  • The toggle is an ordinary button. Opened by popovertarget or command, its menu or drawer needs no script.
  • The pane is decoration. With prefers-reduced-transparency it is opaque, and in forced colors it is the canvas with a line under it.
  • A sticky bar covers the top of the page. Set scroll-padding-top on the scroller to the bar's height, so a link to a heading does not land under it.

#Classes

ClassDescription
navbarThe sticky bar, on a header; a size and scroll-state container named navbar. It has no material until content scrolls under it
navbar-floatingA bar with a margin around it, a surface corner and its material from the start. With navbar
navbar-brandThe mark and the name, as one link home; it also stands alone at the head of a shell or a sidebar
navbar-navThe row of main links, on a nav with a label; hidden below 36rem of bar
navbar-linkA link or a button in the row: a word at rest, on a chip under the pointer
navbar-endThe actions at the far end
navbar-toggleOn the button that opens the links in a menu or a drawer; shown only while navbar-nav is hidden
navbar-progressA reading-progress line along the foot of the bar, which fills as the page scrolls; give it role="presentation"

#Custom properties

PropertyDefaultDescription
--navbar-height3.5remThe bar's minimum height
--navbar-padding-x1remInline padding
--navbar-colorvar(--color-surface-base)The color the pane is tinted with; match the page behind it

#Source

@master/ui/components/navbar.css
/* * Navbar: the bar at the top of a page or an app, holding the brand, the main links and a few actions. * At the top of the page it has no material at all. Once content scrolls under it, it becomes a pane of * frosted glass with a hairline and a short shadow: a scroll-state container query knows when the sticky * bar is stuck. Without that query the pane is always drawn. * The bar is also a size container: below 36rem the links step aside for the toggle that opens them elsewhere. */@layer components {  .navbar {    --navbar-height: 3.5rem;    --navbar-padding-x: 1rem;    --navbar-color: var(--color-surface-base);    position: sticky;    top: 0;    z-index: 20;    display: flex;    align-items: center;    gap: 1rem;    min-width: 0;    min-height: var(--navbar-height);    padding: 0 var(--navbar-padding-x);    color: var(--color-text-strong);    font-size: .875rem;    line-height: 1.4;    container: navbar / inline-size;    /* The pane. It is a layer of its own so that the bar's content never fades with it. */    &::before {      content: '';      position: absolute;      inset: 0;      z-index: -1;      border-radius: inherit;      corner-shape: inherit;      background: color-mix(in oklab, var(--navbar-color) 76%, transparent);      box-shadow: inset 0 -.5px 0 var(--color-line-strong), 0 1px 2px -1px var(--color-shade-1), 0 6px 12px -6px var(--color-shade-2);      backdrop-filter: blur(16px) saturate(1.7);      pointer-events: none;      transition: opacity var(--duration-base) var(--easing-glide);    }    @supports (container-type: scroll-state) {      container-type: inline-size scroll-state;      &::before {        opacity: 0;      }      @container navbar scroll-state(stuck: top) {        &::before {          opacity: 1;        }      }    }    /* Floating: a bar that hovers over the page with a margin around it, always in its material. */    &.navbar-floating {      --navbar-height: 3rem;      --navbar-padding-x: .625rem;      --navbar-color: var(--color-surface-floating);      top: .75rem;      margin: .75rem;      border-radius: calc(var(--radius-surface) * var(--corner-scale));      corner-shape: var(--corner-shape);      &::before {        box-shadow: var(--shadow-lifted);        opacity: 1;      }    }    @media (prefers-reduced-transparency: reduce) {      &::before {        background: var(--navbar-color);        backdrop-filter: none;      }    }    @media (forced-colors: active) {      &::before {        border-bottom: 1px solid CanvasText;        background: Canvas;        opacity: 1;      }    }  }  /* The mark and the name, as one link home. */  .navbar-brand {    display: inline-flex;    flex: none;    align-items: center;    gap: .5rem;    border-radius: calc(var(--radius-chip) * var(--corner-scale));    corner-shape: var(--corner-shape);    color: var(--color-text-strong);    -webkit-tap-highlight-color: transparent;    font-size: .9375rem;    font-weight: 560;    letter-spacing: -.01em;    text-decoration: none;    &:focus-visible {      @apply --focus-ring;    }  }  /* The main links, as a row. Below 36rem of bar they leave; the toggle takes over. */  .navbar-nav {    display: flex;    align-items: center;    gap: .125rem;    min-width: 0;    margin: 0;    padding: 0;    list-style: none;    @container navbar (inline-size < 36rem) {      display: none;    }  }  /* A link is a word at rest; under the pointer it sits on a chip. The current page is in ink. */  .navbar-link {    display: inline-flex;    align-items: center;    gap: .375rem;    min-height: 2rem;    padding: 0 .625rem;    border: 0;    border-radius: calc(var(--radius-control-sm) * var(--corner-scale));    corner-shape: var(--corner-shape);    background: transparent;    color: var(--color-text-muted);    font: inherit;    font-weight: var(--font-weight-control);    white-space: nowrap;    text-decoration: none;    cursor: pointer;    transition-property: color, background-color;    transition-duration: var(--duration-quick);    transition-timing-function: var(--easing-glide);    @media (hover: hover) {      &:hover {        background: var(--color-surface-chip);        color: var(--color-text-strong);      }    }    /* Pressed: a step deeper than the pointer's tone, so a tap is answered where there is no pointer. */    &:active:not([aria-disabled="true"]) {      background: var(--color-surface-inset-hover);      color: var(--color-text-strong);    }    &:is([aria-current="page"], [aria-expanded="true"]) {      color: var(--color-text-strong);    }    &:focus-visible {      @apply --focus-ring;      outline-offset: 0;    }    @media (pointer: coarse) {      min-height: 2.75rem;    }    @media (forced-colors: active) {      &[aria-current="page"] {        text-decoration: underline;      }    }    &[aria-disabled="true"] {      color: var(--color-text-disabled);      cursor: not-allowed;    }  }  /* The actions at the far end. */  .navbar-end {    display: flex;    flex: none;    align-items: center;    gap: .5rem;    margin-inline-start: auto;  }  /* The button that opens the links in a drawer or a menu. It exists only while the links are away. */  .navbar-toggle {    @container navbar (inline-size >= 36rem) {      display: none;    }  }  /*   * Reading progress: a hairline of ink along the foot of the bar that fills as the page scrolls.   * It is drawn only where a scroll timeline can drive it.   */  .navbar-progress {    display: none;    @supports (animation-timeline: scroll()) {      position: absolute;      inset-inline: 0;      bottom: 0;      display: block;      height: 1.5px;      background: var(--color-text-strong);      transform-origin: 0 50%;      pointer-events: none;      animation-name: navbar-progress;      animation-timing-function: linear;      animation-fill-mode: both;      animation-timeline: scroll(root block);      &:dir(rtl) {        transform-origin: 100% 50%;      }    }    @media (forced-colors: active) {      background: Highlight;    }  }}@keyframes navbar-progress {  from {    scale: 0 1;  }  to {    scale: 1 1;  }}