Skip to content
Component

Skip link

A chip that appears only on keyboard focus and jumps straight to the main content.

  • #arc
  • #component
  • #accessibility
  • #keyboard
  • #focus
  • #navigation

Install

One command installs the files and packages this item needs.

Terminalbash
npx shadcn@latest add https://dsui.vercel.app/r/skip-link.json

skip-link.module.csscss
/* Out of sight but still in the tab order: it is moved off the top edge rather than display: none, which would drop it from the order entirely. */
.link { position: fixed; z-index: 100; inset-block-start: var(--space-3); inset-inline-start: var(--space-3); display: inline-flex; min-height: var(--control-height-md); align-items: center; border: 1px solid var(--float-edge, var(--surface-raised)); border-radius: var(--radius-control); padding: 0 var(--space-4); background: var(--surface-raised); color: var(--foreground); box-shadow: var(--shadow-floating); font-size: var(--label-md); font-weight: var(--label-weight); line-height: var(--leading-body); text-decoration: none; opacity: 0; transform: translateY(calc(-100% - var(--space-6))); transition: transform var(--duration-fast) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard); }
.link:focus-visible { opacity: 1; transform: translateY(0); }
/* The global outline would double the chip's own edge, so the chip draws its focus with the accent border instead. */
.link:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .link { transition: none; } }
skip-link.tsxtsx
import type { AnchorHTMLAttributes } from "react";
import styles from "./skip-link.module.css";

export interface SkipLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
  /** The id of the landmark to jump to, with the hash: "#main". */
  href: string;
}

/**
 * The first thing in the tab order: a chip that stays out of sight until keyboard focus reaches it, then floats over
 * the page so the reader can jump straight past the header. It is a floating layer, so its border matches its fill
 * and the shadow draws the edge.
 */
export function SkipLink({ className, href, children, ...props }: SkipLinkProps) {
  return (
    <a {...props} href={href} className={[styles.link, className].filter(Boolean).join(" ")}>
      {children}
    </a>
  );
}

SkipLink.displayName = "SkipLink";

export default SkipLink;