shshadcn-htmx

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.json

2. Use it

components/ui/skip-link.tsx
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
components/ui/skip-link.tsx
/** @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

templates/components/skip-link.html
{% from "components/skip-link.html" import skip_link %}

<body>
  {{ skip_link() }}
  <header>…</header>
  <main id="main" tabindex="-1">…</main>
</body>
View source
templates/components/skip-link.html
{# 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

components/skip-link.tmpl
// First child of <body>, before the header.
{{template "skip-link" .}}
<header>…</header>
<main id="main" tabindex="-1">…</main>
View source
components/skip-link.tmpl
{{/*
  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

lib/my_app_web/components/skip_link.ex
alias ShadcnHtmx.Components.SkipLink

<body>
  <SkipLink.skip_link />
  <header></header>
  <main id="main" tabindex="-1"></main>
</body>
View source
lib/my_app_web/components/skip_link.ex
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

snippets/skip-link.html
<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
snippets/skip-link.html
<!--
  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.

Skip to main content

Repeated banner / navigation (skipped).

Main region — focus lands here.
<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.

Skip to content

Navigation (skipped).

Content region — focus lands here.
<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>

PropTypeDefaultDescription
hrefstring"#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>
childrenChild
Visible label, revealed on focus. Defaults to "Skip to main content".
idstring
Set when another control needs to reference this link.
classstring
Extra Tailwind classes appended to the root element.
hx-*any
Any htmx attribute. Forwarded onto the underlying element.htmxAttribute reference