Navigation

v0.0.4
Navigation

Nav

A vertical list of destinations for a sidebar.

On this page

A nav is a vertical list of destinations for a sidebar or a settings page. Rows are quiet until touched, and the current one rises as a small sheet, the way the current option of a segmented control does. Use .nav on a <nav> and nav-item on each link.

Nav

<div class="tray max-w(240) mx-auto p(8)">  <nav class="nav" aria-label="Workspace">    <a class="nav-item" href="#overview" 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="M5 12 12 5l7 7M7 10v9h10v-9" />      </svg>Overview</a>    <a class="nav-item" href="#projects">      <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">        <rect x="4" y="4" width="7" height="7" rx="1.5" />        <rect x="13" y="4" width="7" height="7" rx="1.5" />        <rect x="4" y="13" width="7" height="7" rx="1.5" />        <rect x="13" y="13" width="7" height="7" rx="1.5" />      </svg>Projects      <span class="nav-end">12</span>    </a>    <a class="nav-item" href="#domains">      <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="9" />        <path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" />      </svg>Domains</a>    <a class="nav-item" aria-disabled="true">      <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">        <rect x="3" y="6" width="18" height="13" rx="2" />        <path d="M3 10h18" />      </svg>Billing</a>  </nav></div>

#Labels and counts

nav-label captions a set of rows. nav-end sets a count, a shortcut or a badge at the end of a row, in tabular figures.

Labels and counts

<nav class="nav max-w(240) mx-auto" aria-label="Inbox">  <div class="nav-label">Inbox</div>  <a class="nav-item" href="#unread" aria-current="page">Unread    <span class="nav-end">24</span>  </a>  <a class="nav-item" href="#mentions">Mentions    <span class="nav-end">3</span>  </a>  <a class="nav-item" href="#archive">Archive</a>  <div class="nav-label">Teams</div>  <a class="nav-item" href="#design">Design</a>  <a class="nav-item" href="#engineering">Engineering    <span class="badge badge-sm nav-end">New</span>  </a></nav>

#Groups

A group is a native <details> with nav-group. Its <summary> is a row with nav-item and nav-toggle, which adds a chevron; the rows under it go in a nav-sub, set in on a hairline guide. The browser keeps the toggle and the keys, and find-in-page opens a folded group that holds a match. The group opens to its own height where interpolate-size is supported, and at once elsewhere.

Groups

<nav class="nav max-w(240) mx-auto" aria-label="Project">  <a class="nav-item" href="#overview">    <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 12 12 5l7 7M7 10v9h10v-9" />    </svg>Overview</a>  <details class="nav-group" open>    <summary class="nav-item nav-toggle">      <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="M12 16V4M7 9l5-5 5 5M5 20h14" />      </svg>Deployments</summary>    <div class="nav-sub">      <a class="nav-item" href="#production" aria-current="page">Production</a>      <a class="nav-item" href="#preview">Preview</a>    </div>  </details>  <details class="nav-group">    <summary class="nav-item nav-toggle">      <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 6h10M18 6h2M4 12h2M10 12h10M4 18h10M18 18h2" />        <circle cx="16" cy="6" r="2" />        <circle cx="8" cy="12" r="2" />        <circle cx="16" cy="18" r="2" />      </svg>Settings</summary>    <div class="nav-sub">      <a class="nav-item" href="#general">General</a>      <a class="nav-item" href="#environment">Environment</a>    </div>  </details></nav>

#Accessibility

  • Wrap the list in a <nav> with a label, and mark the current page with aria-current="page".
  • For a link that is not available, use aria-disabled="true" and remove the href.
  • A nav of sibling pages inside a page is tabs; the path above a page is a breadcrumb.

#Classes

ClassDescription
navThe list, on a nav with a label
nav-itemA destination: a link, a button, or the summary of a group. Quiet until touched; the current one rises as a small sheet
nav-endA count, a shortcut or a badge at the end of a row, in tabular figures
nav-labelA quiet caption over a set of rows
nav-groupA details that folds a set of rows under its summary
nav-toggleOn the summary of a group: adds a chevron at the end of the row, which turns over while the group is open. With nav-item
nav-subThe rows under a group, set in from the icon column on a hairline guide

#Custom properties

PropertyDefaultDescription
--nav-item-height2remRow height; at least 2.75rem on touch screens
--nav-item-radiusvar(--radius-control-sm)Row corner

#Source

@master/ui/components/nav.css
/* * Nav: a vertical list of destinations for a sidebar or a settings page. * Rows are quiet until touched; the current one rises as a small sheet, the way the current option of a * segmented control does. A group is a native <details>, so it folds without script. */@layer components {  .nav {    --nav-item-height: 2rem;    --nav-item-radius: var(--radius-control-sm);    --icon-size: 1rem;    display: flex;    flex-direction: column;    gap: 2px;    min-width: 0;    margin: 0;    padding: 0;    font-size: .84375rem;    line-height: 1.2;    list-style: none;  }  /* A destination: a link with aria-current="page", a button, or the <summary> of a group. */  .nav-item {    --icon-offset: -.0625rem;    --icon-color: var(--color-text-muted);    position: relative;    display: flex;    align-items: center;    gap: .625rem;    min-width: 0;    min-height: var(--nav-item-height);    margin: 0;    padding: 0 .5rem;    border: 0;    border-radius: calc(var(--nav-item-radius) * var(--corner-scale));    corner-shape: var(--corner-shape);    background: transparent;    color: var(--color-text-body);    font: inherit;    font-feature-settings: inherit;    text-align: start;    text-decoration: none;    list-style: none;    appearance: none;    user-select: none;    -webkit-user-select: none;    -webkit-tap-highlight-color: transparent;    cursor: pointer;    touch-action: manipulation;    transition: color var(--duration-quick) var(--easing-glide);    &::-webkit-details-marker {      display: none;    }    @media (hover: hover) {      &:hover:not([aria-current="page"], [aria-selected="true"], :disabled, [aria-disabled="true"]) {        --icon-color: currentColor;        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-current="page"], [aria-selected="true"], :disabled, [aria-disabled="true"]) {      background: var(--color-surface-inset-hover);      color: var(--color-text-strong);    }    &:is([aria-current="page"], [aria-selected="true"]) {      --icon-color: currentColor;      @apply --surface-sheet;      color: var(--color-text-strong);      box-shadow: var(--shadow-raised);    }    &:focus-visible {      @apply --focus-ring;      z-index: 1;    }    &:is(:disabled, [aria-disabled="true"]) {      --icon-color: currentColor;      color: var(--color-text-disabled);      cursor: not-allowed;    }    @media (pointer: coarse) {      min-height: max(var(--nav-item-height), 2.75rem);    }    @media (forced-colors: active) {      &:is([aria-current="page"], [aria-selected="true"]) {        outline: 2px solid Highlight;        outline-offset: -2px;      }    }  }  /* A count, a shortcut or a badge at the end of a row. */  .nav-end {    margin-inline-start: auto;    padding-inline-start: .75rem;    color: var(--color-text-muted);    font-size: .75rem;    font-variant-numeric: tabular-nums;  }  /* A quiet caption over a set of rows. */  .nav-label {    padding: .75rem .5rem .375rem;    color: var(--color-text-muted);    font-size: .71875rem;    &:first-child {      padding-top: .25rem;    }  }  /* A <details> that folds a set of rows under its summary; the chevron turns over when it is open. */  .nav-group {    display: flex;    flex-direction: column;    gap: 2px;    min-width: 0;    interpolate-size: allow-keywords;    &::details-content {      block-size: 0;      overflow-y: clip;      opacity: 0;      transition-property: block-size, opacity, content-visibility;      transition-duration: var(--duration-base);      transition-timing-function: var(--easing-glide);      transition-behavior: allow-discrete;    }    &[open]::details-content {      block-size: auto;      opacity: 1;    }    &[open] .nav-toggle::after {      rotate: x 180deg;    }    /* A closed group nested in an open one keeps its own chevron. */    &[open] .nav-group:not([open]) .nav-toggle::after {      rotate: none;    }    @media (prefers-reduced-motion: reduce) {      &::details-content {        transition-property: opacity, content-visibility;      }    }  }  /* The summary of a group: a row with a chevron at its end. */  .nav-toggle::after {    content: '';    flex: none;    width: .875rem;    height: .875rem;    margin-inline-start: auto;    background: var(--color-text-muted);    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m4.5 6.5 3.5 3.5 3.5-3.5'/%3E%3C/svg%3E") center / contain no-repeat;    transition: rotate var(--duration-base) var(--easing-glide);    @media (prefers-reduced-motion: reduce) {      transition: none;    }    @media (forced-colors: active) {      background: CanvasText;    }  }  /* Rows under a group, set in from the icon column on a hairline guide. */  .nav-sub {    display: flex;    flex-direction: column;    gap: 2px;    margin-block: 2px;    margin-inline: calc(.5rem + var(--icon-size) / 2) 0;    padding-block: 0;    padding-inline: calc(.625rem + var(--icon-size) / 2 - 1px) 0;    border-inline-start: 1px solid var(--color-line-hair);    list-style: none;  }}