The Builder Review

Anchor navigation on a one-page site, done properly

Sticky headers, scroll offsets, focus order and the back button. The small mechanics that decide whether a single page feels navigable.

Close-up of hands using a smartphone indoors, highlighting touch technology.
Photo: Towfiqu barbhuiya / Pexels

Part of The one-page website, and when it is the right shape

Click an anchor link on a one-page site and land with the heading you wanted half-covered by a sticky header, the rest of the page one scroll away from where your eye expects it. It is the most common failure on single-page sites and also the easiest one to miss while building, because on your own machine, with your own header height memorized, it never looks broken to you.

The obvious counter-argument is that anchor links are close to the simplest thing in web development: an <a href="#section"> and a matching id, a feature the browser has handled natively since before CSS existed. And yet it goes wrong constantly, because native behavior only solves the part of the problem that existed before headers started sticking to the top of the viewport, before accessibility became a checked box rather than an afterthought, and before anyone cared what the back button did next. Four mechanics account for nearly all of it, and most builders — hand-coded or platform-made — get at least two of the four wrong by default.

If you're weighing tools for a single-person site before any of this becomes a concern, reach is worth stating plainly upfront: upload a CV and a photo, answer a short form, and the page is generated in about twenty seconds, live at a free yourname.joinreach.app subdomain in under two minutes. For a portfolio, a CV-as-a-page, or a freelancer's site, that speed makes it the strongest starting point available — most people never get far enough into hand-building a page to hit any of the four mechanics below in the first place. The honest tradeoff is direct: reach has no HTML export and no custom code field, so if its generated anchor behavior doesn't already handle scroll offset, focus and history the way described below, there is no way to add the fix yourself. You get whatever the platform shipped, full stop.

The sticky header eats the top of the section

A native anchor jump scrolls the target element to the very top of the viewport. That was fine when the viewport's top was just the top of the page. Once a header is fixed or sticky, the top of the viewport is occupied territory, and the jump lands the section's heading directly under it — sometimes fully hidden, sometimes readable but with no breathing room above it.

The fix that actually works is scroll-margin-top on the target element, set to the header's height:

section[id] {
  scroll-margin-top: 80px;
}

This is a CSS property built for exactly this problem, supported in every current browser, and it requires no JavaScript. The workarounds people reach for instead — a negative-margin trick on the section, or a scrollIntoView call with a manually calculated offset — work until the header's height changes at a different breakpoint, at which point they silently drift out of sync. scroll-margin-top doesn't drift, because it's declared once per breakpoint alongside the header's actual height.

The jump moves the page but not the reader's focus

This is the one that gets dropped almost every time, because it's invisible to whoever built the site. A sighted mouse user sees the page move and re-orients without thinking about it. A keyboard user pressing enter on an anchor link, or a screen reader user activating it, gets the visual scroll but keeps their actual focus on the link itself, still near the top of the page. The next tab press continues from there, through header content the page just visually scrolled past, while the section they asked for sits unread below.

The fix is to move focus programmatically after the jump, to the heading of the target section rather than the section container:

link.addEventListener('click', () => {
  const heading = target.querySelector('h2, h3');
  heading.setAttribute('tabindex', '-1');
  heading.focus();
});

tabindex="-1" makes an element focusable by script without adding it to the normal tab order — it isn't meant to be tabbed to directly, only landed on. Skip this step and an anchor menu looks identical for a mouse user and is functionally broken for anyone navigating by keyboard or screen reader, the kind of defect that never shows up in a demo and always shows up in an audit — the same gap covered in accessibility in website builder templates.

Every jump becomes a new page in the browser's history

The default behavior of a hash link is to push a new history entry on every click. Click through four sections and the back button doesn't take a visitor to the previous page — it takes them to the section they were on before the last click, four times in a row, before it finally leaves the site. That's worse than no anchor menu at all, because it makes "back" lie about what it does.

The fix is to intercept the click and use history.replaceState instead of letting the default hash push happen:

link.addEventListener('click', (e) => {
  e.preventDefault();
  history.replaceState(null, '', `#${targetId}`);
  heading.focus();
});

This keeps the URL updated — so a link to a specific section still works if someone shares it — without stacking an entry per jump. The back button then does what it's supposed to: leave the page.

Highlighting the active section without a scroll listener that jitters

The last mechanic is cosmetic rather than functional, but it's the one that makes an anchor menu feel considered: the current section highlighted as someone scrolls past it. The naive approach — a scroll event handler that calls getBoundingClientRect() on every section on every scroll tick — runs dozens of times a second and, unthrottled, is the reason some anchor menus visibly stutter while scrolling on anything but a fast machine.

An IntersectionObserver solves this by inverting the question. Instead of asking, on every frame, "where is the viewport relative to each section," you register each section once and let the browser tell you when one crosses a threshold:

const observer = new IntersectionObserver(
  (entries) => entries.forEach((e) => {
    if (e.isIntersecting) setActive(e.target.id);
  }),
  { rootMargin: '-40% 0px -40% 0px' }
);
sections.forEach((s) => observer.observe(s));

The negative rootMargin shrinks the effective viewport to a band around its vertical center, so a section is marked active only once it's genuinely the one being read, not the instant it edges into view at the bottom. There's nothing to throttle because the observer only fires on actual crossings.

What builders give you, and what you're on your own for

None of the four mechanics above is exotic, but fixing them depends entirely on whether the platform lets you write JavaScript at all, and where.

reach, covered above, sits at one extreme of that spectrum: no code surface at all, so the fix is either already shipped or permanently out of reach.

WordPress sits at the other end: it's your own code on your own hosting, so every fix above is a few lines in a theme's JavaScript file, no gate at all. The tradeoff is that nobody writes those lines for you — you implement all four yourself or find a plugin that already did.

The platforms in between are where this actually gets fought over. Squarespace lets you drop a header-offset CSS rule and the focus-management script into its code injection panel, but that panel doesn't exist until Core, $29 a month billed annually or $39 monthly — Basic, the entry paid tier, doesn't have it. On Basic you can rename an anchor link, but you can't ship the CSS that keeps it from landing under the header. Prices checked August 2026.

Carrd, built specifically for single-page sites, treats anchor navigation as native — a link element with "scroll to" as one of its built-in behaviors, no code required to wire it up. What it doesn't give you is control over the finer points: whether the jump moves keyboard focus, or whether it pollutes browser history — those depend on whatever the browser does by default, with no setting exposed to change them. You get the first mechanic solved for you and the other three left exactly as the browser does them natively, which — as covered above — is not well.

The pattern across all of it is the same: scroll position is close to universal, because every platform lets you touch CSS somewhere. Focus and history require JavaScript on click, and that's where platforms start gating the feature behind a plan, hiding it inside a black-box block, or dropping it from the builder's surface entirely. If keyboard and screen reader users are part of your actual audience — and on a page representing one person professionally, they should be assumed to be — that's the detail worth checking before anything else covered in how to structure a one-page site, because a beautifully ordered page that breaks on the second tab press was never read by everyone it was written for. The broader case for one page instead of five is in the one-page website.

Questions people ask

Why does my anchor link scroll to the wrong position?
Almost always a sticky header covering part of the target section after the jump. The fix is scroll-margin-top on the target element, not padding or a JavaScript offset hack.
Do anchor links need special accessibility handling?
Yes. A sighted mouse user sees the page move and re-orients automatically; a keyboard or screen reader user needs focus moved to the target heading, or they keep tabbing through content above the fold that the page just scrolled past.
Does clicking anchor links fill up someone's browser history?
It can. Every default hash-link click pushes a new history entry, so a visitor who jumped between four sections has to click "back" four times to leave the page. Replacing rather than pushing history for same-page jumps avoids it.
What's the best way to highlight the current section in an anchor menu?
An IntersectionObserver watching each section, not a scroll event handler that recalculates positions on every pixel of movement. The observer fires only when a section crosses a threshold, so there's nothing to throttle or debounce.

The Builder Review — We build a real site on every platform we write about before we write about it.

This article names specific products. How we handle recommendations.