Fragment Links and In Page Navigation

Link to a spot on the same page with an id and a hash. The basis of tables of contents, skip links, footnotes and back to top links.

Concept

A fragment link points at a location within a document rather than at another document. The href is a hash followed by the id of the target element, and activating it scrolls that element into view and moves the reading position to it.

The fragment is never sent to the server. The browser fetches the page, then handles the hash itself.

Syntax

<a href="#refunds">Jump to refunds</a>

<h2 id="refunds">Refunds</h2>

Two special targets need no id:

<a href="#top">Back to top</a>
<a href="#">Also goes to the top, but avoid this form</a>

A bare hash works but adds a history entry and is easily confused with a placeholder link. Prefer #top.

Example: a table of contents

<nav aria-label="On this page">
  <h2 id="contents">On this page</h2>
  <ol>
    <li><a href="#eligibility">Who can apply</a></li>
    <li><a href="#documents">Documents required</a></li>
    <li><a href="#fees">Fees and payment</a></li>
  </ol>
</nav>

<section>
  <h2 id="eligibility">Who can apply</h2>
  <p>...</p>
</section>

<section>
  <h2 id="documents">Documents required</h2>
  <p>...</p>
</section>

<section>
  <h2 id="fees">Fees and payment</h2>
  <p>...</p>
  <p><a href="#contents">Back to contents</a></p>
</section>

The first focusable element on the page, usually hidden until it receives focus. Without it, a keyboard user tabs through the entire navigation on every single page before reaching the content.

<body>
  <a href="#main-content" class="skip-link">Skip to main content</a>

  <header>
    <nav><!-- thirty links --></nav>
  </header>

  <main id="main-content" tabindex="-1">
    <h1>Admission 2026</h1>
  </main>
</body>

The tabindex="-1" matters. Without it, some browsers scroll to the target but leave keyboard focus back at the link, so the next Tab press returns the reader to the navigation they were trying to escape. A negative tabindex makes the element programmatically focusable without adding it to the tab order.

Example: footnotes

<p>Ridership rose by a fifth.<sup><a href="#fn-1" id="ref-1">1</a></sup></p>

<section>
  <h2>Notes</h2>
  <ol>
    <li id="fn-1">District Transport Review, 2026. <a href="#ref-1">Back to text</a></li>
  </ol>
</section>

The link goes both ways, so a reader who follows a footnote can return to where they were.

The sticky header problem

A fixed header covers the top of the viewport, so a jump target lands underneath it and the reader sees the wrong heading. The fix is one CSS declaration on the target:

:target,
[id] {
  scroll-margin-top: 5rem;   /* height of the fixed header */
}

html {
  scroll-behavior: smooth;   /* optional, animated scrolling */
}

Wrap the smooth scrolling in a prefers-reduced-motion query. Animated scrolling triggers nausea for some readers, and the operating system already carries their preference.

Important rules

  • The id must be unique in the document. A duplicate id means the browser jumps to the first one and ignores the rest.
  • Ids are case sensitive. #Fees will not find id="fees".
  • An id must not contain spaces.
  • The hash never reaches the server, so server side code can never see it.
  • Following a fragment link changes the URL, so the reader can copy or bookmark the exact position.
  • The :target pseudo class matches the currently jumped to element, which is a free way to highlight it.

Common mistakes

  • Linking to #Fees when the id is fees, then concluding fragment links are unreliable.
  • Duplicating an id across several sections.
  • Forgetting tabindex="-1" on a skip link target and leaving keyboard focus behind.
  • Hiding the skip link with display: none, which removes it from the tab order entirely. Move it off screen instead and bring it back on focus.
  • Using href="#" as a placeholder, so a real fragment link and a dead one look identical.
  • Sticky header covering every jump target because scroll-margin-top was never set.

Best practices

  • Put an id on every heading a reader might want to link to. Lower case, hyphenated, derived from the heading text.
  • Keep ids stable. A changed id breaks every bookmark and every inbound deep link.
  • Give every page a skip link, and make it the first focusable element.
  • Set scroll-margin-top on jump targets as soon as the design has a fixed header.
  • Respect prefers-reduced-motion before enabling smooth scrolling.

Practice

  1. Build a page with four sections and a table of contents that links to each, plus a back to contents link at the end of every section.
  2. Add a skip link that is invisible until focused and jumps to the main content. Confirm the next Tab press lands inside the content.
  3. Give a page a sticky header, follow a fragment link and observe the heading disappearing behind it. Fix it with scroll-margin-top.
  4. Style :target with a background colour and describe how it helps a reader who followed a deep link.

Useful resources

Hand picked references for this topic
Written by Lorens Mishra

Software Engineer Notes Management System Administrator

Continue reading

All HTML notes →
HTML

Building a Navigation Menu

A menu is a list of links inside a nav element. Learn the markup that makes it accessible, how to mark the current page, and how a dropdown should be...

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.