Navigation

Skip link

A link that stays out of sight until focused, then lets keyboard users jump past the header to the content.

Built on
<a href>Fragment navigationclip-path
Client JavaScript
None
Works without React
Yes, as HTML and CSS
Source, gzipped
0.8 kB
Dependencies
React
Skip to main content
Code
import { SkipLink } from "@/components/equip/skip-link";

export default function Demo() {
  return (
    <div
      style={{
        position: "relative",
        width: 216,
        border: "1px solid var(--eq-line)",
        borderRadius: "var(--eq-radius-l)",
        background: "var(--eq-surface)",
        fontSize: "var(--eq-text-s)",
      }}
    >
      <SkipLink href="#skip-link-demo-content" />
      <div style={{ display: "flex", alignItems: "center", gap: 12, padding: "8px 12px", borderBottom: "1px solid var(--eq-line)", color: "var(--eq-ink-2)" }}>
        <strong style={{ color: "var(--eq-ink)" }}>Larder</strong>
        <a href="#pricing" style={{ display: "inline-flex", alignItems: "center", minHeight: 24, color: "inherit" }}>Pricing</a>
        <a href="#docs" style={{ display: "inline-flex", alignItems: "center", minHeight: 24, color: "inherit" }}>Docs</a>
      </div>
      <div id="skip-link-demo-content" tabIndex={-1} style={{ padding: "10px 12px", color: "var(--eq-ink-2)", borderRadius: "inherit" }}>
        Tab into this frame and the skip link appears first.
      </div>
    </div>
  );
}
HTMLworks in any framework, with the part's CSS
<div style="position:relative;width:216px;border:1px solid var(--eq-line);border-radius:var(--eq-radius-l);background:var(--eq-surface);font-size:var(--eq-text-s)"><a href="#skip-link-demo-content" class="eq-skip-link">Skip to main content</a><div style="display:flex;align-items:center;gap:12px;padding:8px 12px;border-bottom:1px solid var(--eq-line);color:var(--eq-ink-2)"><strong style="color:var(--eq-ink)">Larder</strong><a href="#pricing" style="display:inline-flex;align-items:center;min-height:24px;color:inherit">Pricing</a><a href="#docs" style="display:inline-flex;align-items:center;min-height:24px;color:inherit">Docs</a></div><div id="skip-link-demo-content" tabindex="-1" style="padding:10px 12px;color:var(--eq-ink-2);border-radius:inherit">Tab into this frame and the skip link appears first.</div></div>

Examples

Two destinations

Each link appears in the same corner as it takes focus, so a second one costs no space. Keep it to two or three.

Skip to main contentSkip to search
The skip links come before the header in the source, so they are the first two stops.
Code
"use client";

import { useId } from "react";
import { SkipLink } from "@/components/equip/skip-link";

export default function Example() {
  const base = useId().replace(/[^\w-]/g, "");
  const frame = {
    position: "relative" as const,
    width: "100%",
    maxWidth: 420,
    border: "1px solid var(--eq-line)",
    borderRadius: "var(--eq-radius-l)",
    background: "var(--eq-surface)",
    fontSize: "var(--eq-text-s)",
    color: "var(--eq-ink-2)",
  };
  const link = { display: "inline-flex", alignItems: "center", minHeight: 24, color: "inherit" };
  return (
    <div style={frame}>
      <SkipLink href={`#content-${base}`} />
      <SkipLink href={`#search-${base}`}>Skip to search</SkipLink>
      <div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: 12, padding: "8px 12px", borderBottom: "1px solid var(--eq-line)" }}>
        <strong style={{ color: "var(--eq-ink)" }}>Field notes</strong>
        <a href="#archive" style={link}>Archive</a>
        <a href="#authors" style={link}>Authors</a>
        <a href="#about" style={link}>About</a>
      </div>
      <div style={{ padding: "10px 12px", borderBottom: "1px solid var(--eq-line)" }}>
        <label htmlFor={`search-${base}`} style={{ display: "block", marginBottom: 4, color: "var(--eq-ink)", fontWeight: "var(--eq-weight-strong)" }}>
          Search articles
        </label>
        <input
          id={`search-${base}`}
          type="search"
          style={{
            appearance: "none",
            boxSizing: "border-box",
            width: "100%",
            height: "var(--eq-h-m)",
            paddingInline: "var(--eq-space-3)",
            border: "1px solid var(--eq-line-strong)",
            borderRadius: "var(--eq-radius-m)",
            background: "var(--eq-raised)",
            color: "var(--eq-ink)",
            font: "inherit",
          }}
        />
      </div>
      <div id={`content-${base}`} tabIndex={-1} style={{ padding: "10px 12px", borderRadius: "inherit" }}>
        The skip links come before the header in the source, so they are the first two stops.
      </div>
    </div>
  );
}

Shown while focused

Here the link is given a static position so you can see its focused look; in a page it floats over the header's corner.

Code
import { SkipLink } from "@/components/equip/skip-link";

export default function Example() {
  return (
    <div style={{ display: "flex", flexWrap: "wrap", gap: 12 }}>
      <SkipLink href="#docs-content" style={{ position: "static", clipPath: "none", opacity: 1, translate: "none" }}>
        Skip to documentation
      </SkipLink>
      <SkipLink href="#checkout-form" style={{ position: "static", clipPath: "none", opacity: 1, translate: "none" }}>
        Skip to checkout form
      </SkipLink>
    </div>
  );
}
HTMLworks in any framework, with the part's CSS
<div style="display:flex;flex-wrap:wrap;gap:12px">
  <a style="position:static;clip-path:none;opacity:1;translate:none" href="#docs-content" class="eq-skip-link">Skip to documentation</a>
  <a style="position:static;clip-path:none;opacity:1;translate:none" href="#checkout-form" class="eq-skip-link">Skip to checkout form</a>
</div>

Props

PropTypeDefaultWhat it does
hrefstring"#main"A fragment pointing at the main content's id.
childrenReactNode"Skip to main content"The link text.
...restComponentProps<"a">noneEvery native anchor attribute.

Anatomy

root.eq-skip-linkThe link. Clipped to nothing until it has focus, then drawn in the corner of its positioned ancestor.

Keyboard

TabFrom the top of the page, the first stop; the link appears.
EnterMoves to the main content, so the next Tab starts there.

Notes

  • Put it first in the body, or first in the header, so it is the first thing Tab reaches.
  • Give the target tabIndex={-1} so focus moves onto it in every browser, not just the scroll position.
  • It is clipped, not hidden with display or visibility, so it stays in the tab order and in the accessibility tree.
  • It positions against its nearest positioned ancestor; give the page or header position: relative or sticky.
  • Renders on the server with no client JavaScript.

Source

Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.

components/equip/skip-link.tsx15 lines
import type { ComponentProps } from "react";
import "./skip-link.css";

export type SkipLinkProps = ComponentProps<"a"> & {
  /** The id of the main content, as a fragment. Give that element tabIndex={-1} so focus lands on it. */
  href?: string;
};

export function SkipLink({ href = "#main", className, children = "Skip to main content", ...rest }: SkipLinkProps) {
  return (
    <a {...rest} href={href} className={["eq-skip-link", className].filter(Boolean).join(" ")}>
      {children}
    </a>
  );
}
components/equip/skip-link.css46 lines
@layer equip {
  /* Out of sight until it has focus. Clipped rather than display: none, so it stays in the tab order. */
  .eq-skip-link {
    position: absolute;
    inset-block-start: var(--eq-space-2);
    inset-inline-start: var(--eq-space-2);
    z-index: 100;
    display: inline-flex;
    align-items: center;
    height: var(--eq-h-m);
    padding-inline: var(--eq-space-3);
    border-radius: var(--eq-radius-m);
    background: var(--eq-solid);
    color: var(--eq-solid-ink);
    font: var(--eq-weight-strong) var(--eq-text-m) / 1 var(--eq-font);
    text-decoration: none;
    white-space: nowrap;
    box-shadow: var(--eq-shadow);
    clip-path: inset(50%);
    opacity: 0;
    translate: 0 -4px;
    transition:
      opacity var(--eq-dur) var(--eq-ease),
      translate var(--eq-dur) var(--eq-ease),
      scale var(--eq-dur) var(--eq-ease);
  }

  .eq-skip-link:focus {
    clip-path: none;
    opacity: 1;
    translate: none;
    transition:
      opacity var(--eq-dur) var(--eq-ease),
      translate var(--eq-dur-move) var(--eq-spring),
      scale var(--eq-dur) var(--eq-ease);
  }

  .eq-skip-link:focus-visible {
    outline: 2px solid var(--eq-focus);
    outline-offset: 2px;
  }

  .eq-skip-link:active {
    scale: var(--eq-press);
  }
}

EqUIp is in beta: free for non-commercial use, no warranty, and things can change. Terms

Nothing picked yet. Press A on a part, or use the plus on the board, and the install command builds here.

Loading the index.

Loading the index