Navigation

v0.0.4
Navigation

Skip link

The first link of a page, shown while it has focus, that jumps past the navigation.

On this page

A skip link is the first link of a page. It jumps past the navigation to the content, so a reader who uses the keyboard does not tab through every link of the header on every page. It is in the document for everyone and on the screen only while it has focus: nobody who uses a pointer sees it.

Skip link

<a class="skip" href="#content">Skip to content</a><header class="navbar">  <a class="navbar-brand" href="#home">Atlas</a>  <nav class="navbar-nav" aria-label="Main">    <a class="navbar-link" href="#projects" aria-current="page">Projects</a>    <a class="navbar-link" href="#deployments">Deployments</a>    <a class="navbar-link" href="#settings">Settings</a>  </nav></header><main class="p-lg" id="content" tabindex="-1">  <h1 class="heading">Projects</h1>  <p class="lead lead-sm">Fourteen projects in three teams.</p></main>
Click inside the preview, then press Tab.

#Markup

Two things make it work:

  1. The link is the first focusable element of the page, before the header, and its href is the id of the main region.
  2. The target has tabindex="-1". A fragment link scrolls to its target in every browser, but focus moves there only if the target can take focus; without it the next Tab goes back to the header the reader just skipped.

Write what the link does in its text: "Skip to content". A page with a long sidebar may offer a second one, "Skip to navigation", after the first.

#How it shows

At rest the link is above the top edge of the viewport, transparent, and cannot be pressed, but it keeps its place in the tab order: an element that is display: none would be skipped itself. On focus it drops in 8px from the top corner as a small control of ink, over a sticky navbar and anything else on the page. --skip-layer is the z-index it stands at.

It stands in a corner of the viewport because that is its job, so it is one of the few components that place themselves. On a phone with a notch it keeps clear of the safe area.

#Accessibility

  • Every page with navigation before its content needs one. WCAG 2.4.1, Bypass Blocks, asks for a way past content that repeats on every page, and a skip link is the usual way to meet it.
  • The link is shown on :focus, not only on :focus-visible, so it also appears for a reader who reaches it with a switch or a screen reader's own navigation.
  • With reduced motion it appears in place, without the drop.
  • Name the target with a heading or aria-label when the page has more than one <main>-like region, so the place a reader lands in says what it is.

#Classes

ClassDescription
skipThe link, on an anchor that points at an id: fixed 8px inside the top corner as a small control of ink, out of sight until it has focus. A long label wraps inside the viewport

#Custom properties

PropertyDefaultDescription
--skip-layer1000The z-index it stands at, over a sticky navbar

#Source

@master/ui/components/skip.css
/* * Skip: the first link of a page, which jumps past the navigation to the content. * It is in the document for everyone and on the screen only while it has focus: a reader who starts with the * Tab key meets it first, and nobody else sees it. Focused, it drops in at the top corner of the viewport as a * small control of ink, over everything else on the page. * It stands there because standing there is its job: fixed, 8px from the corner, clear of a notch. * Geometry: 38px tall with a control's corner, as a button is. */@layer components {  .skip {    --skip-layer: 1000;    position: fixed;    z-index: var(--skip-layer);    inset-block-start: max(.5rem, env(safe-area-inset-top));    inset-inline-start: max(.5rem, env(safe-area-inset-left));    display: inline-flex;    align-items: center;    box-sizing: border-box;    min-height: 2.375rem;    padding: 0 1rem;    border-radius: calc(var(--radius-control) * var(--corner-scale));    corner-shape: var(--corner-shape);    @apply --control-ink;    color: var(--color-text-inverse);    font: var(--font-weight-control) .84375rem/1.2 var(--font-family-sans);    font-feature-settings: inherit;    text-decoration: none;    /* A long label wraps inside the viewport instead of leaving it. */    max-width: calc(100vw - 1rem);    padding-block: .5rem;    text-wrap: balance;    /* At rest it is above the viewport and cannot be pressed; it keeps its place in the tab order. */    opacity: 0;    translate: 0 calc(-100% - 1rem);    pointer-events: none;    transition: translate var(--duration-quick) var(--easing-glide), opacity var(--duration-quick) var(--easing-glide);    &:focus {      opacity: 1;      translate: 0 0;      pointer-events: auto;    }    &:focus-visible {      @apply --focus-ring;    }    @media (pointer: coarse) {      min-height: 2.75rem;    }    @media (prefers-reduced-motion: reduce) {      transition: none;    }    @media (forced-colors: active) {      outline: 1px solid ButtonText;      outline-offset: -1px;    }  }}