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
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
| Prop | Type | Default | What it does |
|---|---|---|---|
href | string | "#main" | A fragment pointing at the main content's id. |
children | ReactNode | "Skip to main content" | The link text. |
...rest | ComponentProps<"a"> | none | Every native anchor attribute. |
Anatomy
| root | .eq-skip-link | The link. Clipped to nothing until it has focus, then drawn in the corner of its positioned ancestor. |
|---|
Keyboard
| Tab | From the top of the page, the first stop; the link appears. |
|---|---|
| Enter | Moves 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);
}
}