Components
Skip Link
The first focusable element on the page — a Skip to main content link that stays visually hidden until a keyboard user tabs to it, then jumps focus past the repeated banner and navigation to the <main> landmark. A native <a href="#main"> with a CSS focus reveal — no JavaScript at all.
Installation
One file per stack — no npm package, no build step required. Use the shadcn CLI for JSX projects, or copy the source straight into your template directory.
1. Install via the shadcn CLI
npx shadcn@latest add http://localhost/r/skip-link.json2. Use it
import { SkipLink } from "@/components/ui/skip-link"
// First child of <body>, before the header.
<body>
<SkipLink />
<header>…</header>
<main id="main" tabindex={-1}>…</main>
</body>Or copy the source manually
/** @jsxImportSource hono/jsx */
import type { PropsWithChildren } from "hono/jsx"
import { cn, type ClassValue } from "@/registry/lib/cn"
// Skip Link — shadcn-htmx, htmx v4 + Tailwind v4.
//
// A "Skip to main content" link: the FIRST focusable element in the document,
// visually hidden until it receives keyboard focus, that jumps focus past the
// repeated banner/nav to the page's main landmark. This is the foundational
// keyboard entry point of the page shell — zero JavaScript, pure platform.
//
// Accessibility contract — WAI-ARIA APG Landmark Regions practice:
// repos/aria-practices/content/practices/landmark-regions/landmark-regions-practice.html
// "Landmark regions can also be used as targets for 'skip links' and by
// browser extensions to enhance keyboard navigation." (Introduction)
// The skip link's destination is therefore a landmark — by default <main>
// (the Main landmark, exactly one per page):
// repos/mdn/files/en-us/web/accessibility/aria/reference/roles/main_role/index.md
//
// Built on a native <a href="#main"> (MDN: an <a> with href has the implicit
// `link` role + Enter-to-activate + focus-moves-to-fragment-target, all from
// the platform — no JS, no role/tabindex needed):
// repos/mdn/files/en-us/web/html/reference/elements/a/index.md
//
// Reveal-on-focus is CSS-only. At rest the link is `sr-only` (the standard
// visually-hidden recipe — Tailwind's sr-only: position:absolute; 1px box;
// clip-path:inset(50%) — repos/tailwindcss/packages/tailwindcss/src/utilities.ts).
// Because that box is clipped and 1px, a pointer can't land on it, so the only
// way it gains focus is a keyboard Tab; on :focus we flip to `not-sr-only` and
// position it in the top-left. We key the reveal off :focus (not :focus-visible)
// so the revealed pill is consistent for every focus source, while staying
// invisible for mouse users who never tab to it.
//
// Visual styling mirrors the rest of the library: bg-primary pill on a ring,
// using only existing theme tokens.
const base =
// Visually hidden at rest — the standard SR-only recipe. A clipped 1px box
// can't be hit by a pointer, so focus only arrives via keyboard Tab.
"sr-only " +
// On focus, undo the clip and pin to the top-left as a real pill. `absolute`
// positions it against the nearest positioned ancestor (the page <body> in
// production; a relative wrapper in the docs preview).
"focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 " +
"focus:inline-flex focus:items-center focus:gap-2 focus:rounded-md " +
"focus:bg-primary focus:px-4 focus:py-2 focus:text-sm focus:font-medium " +
"focus:text-primary-foreground focus:shadow-md focus:no-underline " +
// Same focus ring as the rest of the library so the landing point is obvious.
"focus:outline-none focus:ring-[3px] focus:ring-ring/50"
export function skipLinkClasses(opts?: { class?: ClassValue }): string {
return cn(base, opts?.class)
}
type SkipLinkProps = PropsWithChildren<{
// Fragment of the target landmark. Defaults to "#main" — the Main landmark.
// Must match the id of the element focus should jump to (typically <main id="main">).
href?: string
class?: ClassValue
id?: string
// htmx v4 attributes (subset). Forwarded onto the <a> via {...rest}. A skip
// link rarely needs htmx, but boosting same-origin links is supported.
// See repos/htmx/www/reference.md.
"hx-get"?: string
"hx-boost"?: string
}>
export function SkipLink(props: SkipLinkProps) {
const { children, href = "#main", class: className, id, ...rest } = props
return (
<a
id={id}
href={href}
data-slot="skip-link"
class={skipLinkClasses({ class: className })}
{...rest}
>
{children ?? "Skip to main content"}
</a>
)
}
1. Save the file
Copy skip-link.html into templates/components/.
2. Use it
{% from "components/skip-link.html" import skip_link %}
<body>
{{ skip_link() }}
<header>…</header>
<main id="main" tabindex="-1">…</main>
</body>View source
{# Skip Link macro — shadcn-htmx, htmx v4 + Tailwind v4.
Mirrors registry/ui/skip-link.tsx so a Python/Flask/FastAPI/Django project
renders the same markup our docs site renders.
A "Skip to main content" link: the FIRST focusable element in the document,
visually hidden until keyboard focus, that jumps focus to the page's main
landmark. Pure platform — a native <a href="#main"> with a CSS focus reveal,
zero JavaScript.
Place it as the very first child of <body>, before the header/nav, and give
your main landmark the matching id:
{% from "components/skip-link.html" import skip_link %}
<body>
{{ skip_link() }}
<header>…</header>
<main id="main">…</main>
</body>
Accessibility contract — WAI-ARIA APG Landmark Regions practice:
repos/aria-practices/content/practices/landmark-regions/landmark-regions-practice.html
The <a href> gives the link role + Enter-to-activate + focus-jump for free.
All hx-* / data-* / aria-* attributes pass through via **attrs (underscores
become dashes, so hx_boost="true" emits hx-boost="true"). #}
{% macro skip_link(label="Skip to main content", href="#main", id=none, extra_class="", **attrs) %}
{%- set base -%}
sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:inline-flex focus:items-center focus:gap-2 focus:rounded-md focus:bg-primary focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:text-primary-foreground focus:shadow-md focus:no-underline focus:outline-none focus:ring-[3px] focus:ring-ring/50
{%- endset -%}
<a href="{{ href }}"
{%- if id %} id="{{ id }}"{% endif %}
data-slot="skip-link"
class="{{ base }} {{ extra_class }}"
{%- for k, v in attrs.items() %} {{ k|replace('_', '-') }}="{{ v }}"{% endfor -%}
>{{ label }}</a>
{% endmacro %}
1. Save the file
Add skip-link.tmpl alongside your templates.
2. Use it
// First child of <body>, before the header.
{{template "skip-link" .}}
<header>…</header>
<main id="main" tabindex="-1">…</main>View source
{{/*
Skip Link template — shadcn-htmx, htmx v4 + Tailwind v4.
Mirrors registry/ui/skip-link.tsx for Go projects using html/template.
A "Skip to main content" link: the FIRST focusable element in the document,
visually hidden until keyboard focus, that jumps focus to the page's main
landmark. Pure platform — a native <a href="#main"> with a CSS focus reveal,
zero JavaScript.
Place it as the very first child of <body>, before the header/nav, and give
your main landmark the matching id (<main id="main">).
Usage in your code:
type SkipLinkArgs struct {
Label string // default "Skip to main content"
Href string // default "#main"
ID string
Class string // extra Tailwind classes
Attrs map[string]string // hx-boost, data-*, aria-*, …
}
tpl := template.Must(template.New("").ParseFiles("components/skip-link.tmpl"))
tpl.ExecuteTemplate(w, "skip-link", SkipLinkArgs{})
Accessibility contract — WAI-ARIA APG Landmark Regions practice:
repos/aria-practices/content/practices/landmark-regions/landmark-regions-practice.html
The <a href> gives the link role + Enter-to-activate + focus-jump for free.
*/}}
{{define "skip-link"}}
{{- $label := or .Label "Skip to main content" -}}
{{- $href := or .Href "#main" -}}
{{- $base := "sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:inline-flex focus:items-center focus:gap-2 focus:rounded-md focus:bg-primary focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:text-primary-foreground focus:shadow-md focus:no-underline focus:outline-none focus:ring-[3px] focus:ring-ring/50" -}}
<a href="{{$href}}"
{{- if .ID}} id="{{.ID}}"{{end}}
data-slot="skip-link"
class="{{$base}}{{if .Class}} {{.Class}}{{end}}"
{{- range $k, $v := .Attrs}} {{$k}}="{{$v}}"{{end -}}
>{{$label}}</a>
{{end}}
1. Save the file
Drop skip_link.ex into lib/my_app_web/components/.
2. Use it
alias ShadcnHtmx.Components.SkipLink
<body>
<SkipLink.skip_link />
<header>…</header>
<main id="main" tabindex="-1">…</main>
</body>View source
defmodule ShadcnHtmx.Components.SkipLink do
@moduledoc """
Skip Link — shadcn-htmx, htmx v4 + Tailwind v4 for Phoenix.
Mirrors registry/ui/skip-link.tsx so a Phoenix LiveView project renders the
same markup our docs site renders. Works with plain HEEx templates too —
htmx / data / aria attributes pass straight through via `:rest`.
A "Skip to main content" link: the FIRST focusable element in the document,
visually hidden until keyboard focus, that jumps focus to the page's main
landmark. Pure platform — a native `<a href="#main">` with a CSS focus
reveal, zero JavaScript.
Place it as the very first child of `<body>`, before the header/nav, and give
your main landmark the matching id (`<main id="main">`).
## Examples
<.skip_link />
<.skip_link href="#content">Skip to content</.skip_link>
Accessibility contract — WAI-ARIA APG Landmark Regions practice:
repos/aria-practices/content/practices/landmark-regions/landmark-regions-practice.html
The `<a href>` gives the link role + Enter-to-activate + focus-jump for free.
"""
use Phoenix.Component
@base "sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 " <>
"focus:inline-flex focus:items-center focus:gap-2 focus:rounded-md " <>
"focus:bg-primary focus:px-4 focus:py-2 focus:text-sm focus:font-medium " <>
"focus:text-primary-foreground focus:shadow-md focus:no-underline " <>
"focus:outline-none focus:ring-[3px] focus:ring-ring/50"
attr :href, :string, default: "#main"
attr :class, :string, default: nil
attr :rest, :global,
include: ~w(id hx-get hx-boost)
slot :inner_block
def skip_link(assigns) do
assigns = assign(assigns, :base_class, @base)
~H"""
<a href={@href} class={[@base_class, @class]} data-slot="skip-link" {@rest}>
{if @inner_block == [], do: "Skip to main content", else: render_slot(@inner_block)}
</a>
"""
end
end
1. Save the file
Paste the markup; relies only on theme tokens.
2. Use it
<body>
<a href="#main" data-slot="skip-link"
class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4
focus:z-50 focus:rounded-md focus:bg-primary focus:px-4 focus:py-2
focus:text-primary-foreground …">
Skip to main content
</a>
<header>…</header>
<main id="main" tabindex="-1">…</main>
</body>View source
<!--
shadcn-htmx — raw HTML skip-link snippet.
A "Skip to main content" link: the FIRST focusable element in the document,
visually hidden until keyboard focus, that jumps focus past the repeated
banner/nav to the page's main landmark. Pure platform — a native
<a href="#main"> with a CSS-only focus reveal. NO JavaScript.
How it works (no framework, no script):
- At rest the link carries `sr-only`: position:absolute, a clipped 1px box.
A pointer can't land on a 1px clipped box, so the only way it gains focus
is a keyboard Tab — exactly who a skip link is for.
- On :focus the `focus:not-sr-only focus:absolute …` utilities undo the clip
and pin it to the top-left as a visible pill.
- The native <a href="#main"> gives the link role, Enter-to-activate, and
focus-jump to the target for free (MDN <a> element).
Place it as the VERY FIRST child of <body>, before the header/nav, and give
your main landmark the matching id:
<body>
<!-- skip link here, first -->
<header>…</header>
<main id="main" tabindex="-1">…</main>
</body>
tabindex="-1" on <main> is optional but recommended: some browsers move the
caret to the fragment target but not keyboard focus; -1 lets the link move
focus into the landmark reliably without adding <main> to the normal tab order.
Accessibility contract — WAI-ARIA APG Landmark Regions practice:
repos/aria-practices/content/practices/landmark-regions/landmark-regions-practice.html
Requirements:
1. Tailwind CSS v4 (or the Play CDN for quick experiments):
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
2. The shadcn CSS variables (--primary, --primary-foreground, --ring, …) —
copy the :root / .dark blocks from app/styles/input.css.
-->
<!-- ─── Default: "Skip to main content" → #main ──────────────────────── -->
<a href="#main" data-slot="skip-link"
class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:inline-flex focus:items-center focus:gap-2 focus:rounded-md focus:bg-primary focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:text-primary-foreground focus:shadow-md focus:no-underline focus:outline-none focus:ring-[3px] focus:ring-ring/50">
Skip to main content
</a>
<!-- ─── Custom target + label ───────────────────────────────────────── -->
<a href="#content" data-slot="skip-link"
class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:inline-flex focus:items-center focus:gap-2 focus:rounded-md focus:bg-primary focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:text-primary-foreground focus:shadow-md focus:no-underline focus:outline-none focus:ring-[3px] focus:ring-ring/50">
Skip to content
</a>
<!-- ─── Full page shell showing where it goes ───────────────────────── -->
<body>
<a href="#main" data-slot="skip-link"
class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:inline-flex focus:items-center focus:gap-2 focus:rounded-md focus:bg-primary focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:text-primary-foreground focus:shadow-md focus:no-underline focus:outline-none focus:ring-[3px] focus:ring-ring/50">
Skip to main content
</a>
<header>Site banner + navigation…</header>
<main id="main" tabindex="-1">Main content starts here.</main>
</body>
Examples
Tab to reveal
Click inside the canvas, then press Tab. The hidden link appears top-left; press Enter to jump focus to the main region.
At rest the link is sr-only — a clipped 1px box no pointer can hit, so the only way it gains focus is a keyboard Tab. On :focus the focus:not-sr-only utilities undo the clip and pin it to the top-left as a pill. Because it's a real <a href>, the platform gives you the link role, Enter activation and the focus-jump for free. In the demo the link targets a local region; on a real page it points at #main.
<body>
<SkipLink />
<header>…</header>
<main id="main" tabindex={-1}>…</main>
</body><body>
{{ skip_link() }}
<header>…</header>
<main id="main" tabindex="-1">…</main>
</body>{{template "skip-link" .}}
<header>…</header>
<main id="main" tabindex="-1">…</main><body>
<SkipLink.skip_link />
<header>…</header>
<main id="main" tabindex="-1">…</main>
</body><div class="relative w-full max-w-md rounded-md border bg-card p-4">
<a href="#sl-demo-main" data-slot="skip-link" class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:inline-flex focus:items-center focus:gap-2 focus:rounded-md focus:bg-primary focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:text-primary-foreground focus:shadow-md focus:no-underline focus:outline-none focus:ring-[3px] focus:ring-ring/50">Skip to main content</a>
<p class="mb-3 text-xs text-muted-foreground">Repeated banner / navigation (skipped).</p>
<div id="sl-demo-main" tabindex="-1" class="rounded-md border border-dashed p-3 text-sm focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none">Main region — focus lands here.</div>
</div>Custom target and label
Point the link at any landmark id and override the visible label.
The default target is #main, but a page can offer more than one bypass link (e.g. skip to the search, or to a primary navigation). Pass href with the fragment of the destination landmark's id and a custom label as children.
<SkipLink href="#content">Skip to content</SkipLink>{{ skip_link("Skip to content", href="#content") }}{{template "skip-link" (dict "Label" "Skip to content" "Href" "#content")}}<SkipLink.skip_link href="#content">Skip to content</SkipLink.skip_link><div class="relative w-full max-w-md rounded-md border bg-card p-4">
<a href="#sl-demo-content" data-slot="skip-link" class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:inline-flex focus:items-center focus:gap-2 focus:rounded-md focus:bg-primary focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:text-primary-foreground focus:shadow-md focus:no-underline focus:outline-none focus:ring-[3px] focus:ring-ring/50">Skip to content</a>
<p class="mb-3 text-xs text-muted-foreground">Navigation (skipped).</p>
<div id="sl-demo-content" tabindex="-1" class="rounded-md border border-dashed p-3 text-sm focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none">Content region — focus lands here.</div>
</div>Further reading
API Reference
<SkipLink>
| Prop | Type | Default | Description |
|---|---|---|---|
href | string | "#main" | Fragment of the target landmark. Must match the id of the element focus should jump to (typically <main id="main">). The native <a href> gives the link role, Enter-to-activate and the focus-jump for free.MDN<a href> |
children | Child | — | Visible label, revealed on focus. Defaults to "Skip to main content". |
id | string | — | Set when another control needs to reference this link. |
class | string | — | Extra Tailwind classes appended to the root element. |
hx-* | any | — | Any htmx attribute. Forwarded onto the underlying element.htmxAttribute reference |