Skip to content
goodtabs UI
BrowseBrowse components

Skip link

A skip to content link, hidden until focused, then a primary pill at the top left.

npx shadcn@latest add https://ui.goodtabs.co/r/skip-link.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/skip-link

Skip to contentSkip to content

The pill above is forced visible for the preview. Press Tab to reveal the real one.

A real skip link you reveal with Tab, plus one forced visible for the preview.

Custom target

Skip to articleSkip to article

Why skip links matter

Keyboard users land here with one Tab and one Enter, past the header.

Points at an article id with its own label. The target takes focus.

In a page shell

Skip to content
Northwind

Overview

Tab once from the top of this page and the pill covers the logo, as it would on a real site.

First child of a mock page with a header, shown where it lands when focused.

Usage

import { SkipLink } from "@/components/ui/skip-link"

<body>
  <SkipLink href="#content" />
  <Header />
  <main id="content" tabIndex={-1}>...</main>
</body>

Props

PropTypeDefaultDescription
hrefstring"#content"The id of the main content, with the hash.
childrenReactNode"Skip to content"Link text.
forceVisiblebooleanfalseShows the pill in normal flow without focus. Docs and previews only.

Source

skip-link/skip-link.tsx
import * as React from "react";
import { cn } from "@/lib/utils";

type SkipLinkProps = Omit<React.ComponentProps<"a">, "href" | "children"> & {
  /** The id of the main content, with the hash. */
  href?: string;
  /** Link text. */
  children?: React.ReactNode;
  /**
   * Render the pill as it looks when focused, in normal flow, without waiting for focus.
   * For docs and previews only. Never ship it on.
   */
  forceVisible?: boolean;
};

/**
 * A skip to content link. Hidden for sighted users until it takes focus, then a primary pill
 * at the top left of its nearest positioned ancestor. Put it first in the body.
 */
export function SkipLink({
  href = "#content",
  children = "Skip to content",
  forceVisible = false,
  className,
  ...props
}: SkipLinkProps) {
  return (
    <a
      data-slot="skip-link"
      href={href}
      data-force-visible={forceVisible || undefined}
      className={cn(
        "z-50 inline-flex h-8 items-center rounded-full bg-primary px-4 text-sm font-medium whitespace-nowrap text-primary-foreground shadow-md outline-offset-2 outline-ring focus:absolute focus:top-3 focus:left-3 focus:outline-2",
        forceVisible ? "outline-2" : "sr-only focus:not-sr-only",
        className,
      )}
      {...props}
    >
      {children}
    </a>
  );
}

For AI agents

Hand your agent this page, the markdown at /components/skip-link/prompt.md, or the Copy AI prompt button above.

When to use

  • On every page with a header or sidebar before the main content
  • As the first focusable element in the body, before the logo
  • In app shells where the sidebar has many links

When not to use

  • On a page where the main content is already the first thing in the tab order
  • As an in-page table of contents: it skips one block, it is not navigation
  • With forceVisible in production: that prop is for docs and previews

Agent notes

  • Put it first in the body and give the target an id plus tabIndex={-1} so focus lands there in every browser
  • When focused it is absolutely positioned at the top left of the nearest positioned ancestor, so leave the body unpositioned
  • forceVisible renders the pill in normal flow for previews only; the real link is sr-only until focused
  • It is a plain anchor, not next/link, because it targets an id on the same page