Skip link
A skip to content link, hidden until focused, then a primary pill at the top left.
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/skip-link
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
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
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
| Prop | Type | Default | Description |
|---|---|---|---|
| href | string | "#content" | The id of the main content, with the hash. |
| children | ReactNode | "Skip to content" | Link text. |
| forceVisible | boolean | false | Shows the pill in normal flow without focus. Docs and previews only. |
Source
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