Skip to content
← writing

Two lines so anchor links don't hide under your header

css
ux
accessibility

You add a sticky header, ship some in-page anchor links, and a week later someone reports that clicking a link "scrolls to the wrong place." It doesn't, really. The browser scrolls the target to the very top of the viewport, which is exactly where your sticky header is sitting. The heading ends up tucked behind it.

Two short rules fix it:

html {
  scroll-behavior: smooth;
}
 
[id] {
  scroll-margin-top: 60px;
}

Toggle the offset below and click the links. With it off, every heading lands under the bar:

Intro

Section 1. Click a link above and watch where this heading lands. With the offset off, the sticky bar covers it.

Install

Section 2. Click a link above and watch where this heading lands. With the offset off, the sticky bar covers it.

Usage

Section 3. Click a link above and watch where this heading lands. With the offset off, the sticky bar covers it.

FAQ

Section 4. Click a link above and watch where this heading lands. With the offset off, the sticky bar covers it.

What each line does

scroll-behavior: smooth on the root makes anchor jumps animate instead of teleport. Small thing, but it tells the eye where it just went.

scroll-margin-top is the real fix. It reserves space at the top of an element for scrolling purposes only, so when the browser scrolls that element into view it stops short by that much. Set it to your header height (a bit more if you want breathing room) and the heading clears the bar every time. The [id] selector applies it to anything you can link to, so you don't have to remember per heading. It also kicks in for keyboard focus and the browser's find-on-page, not just clicks.

A couple of notes

Respect motion preferences. Wrap the smooth scroll so people who've asked for less motion don't get it:

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

And if your header height changes (taller on mobile, say), drive the offset off a variable instead of hard-coding 60px:

:root { --header: 56px; }
[id] { scroll-margin-top: calc(var(--header) + 8px); }

That's the whole thing. It's the kind of fix nobody notices when it's there, and everybody notices when it's missing.

Ask your agent to implement this

Read the full writeup at https://seangeng.com/writing/anchor-links-that-dont-hide.md and implement it in my project.

It covers: Two lines so anchor links don't hide under your header — Smooth in-page scrolling plus scroll-margin-top, so jumping to a heading lands it below your sticky header instead of tucked behind it. The fix everyone forgets until a link feels broken.

Requirements:
- Follow the technique/approach exactly as described in the writeup.
- Adapt names, colors, and styling to my project's existing conventions.
- If it's a component, make it reusable with sensible props and TypeScript types.
- Keep it accessible: semantic HTML, keyboard support, and respect prefers-reduced-motion.
- When done, tell me which files you created or changed and how to use it.

Paste into Claude Code, Codex, Cursor, or any agent. view raw .md