Question

How to Offset HTML Anchors for a Fixed Header

cssjavascripthtmlanchoroffset

Question

I am trying to improve how in-page anchors work on a page with a fixed header.

Because the header is fixed to the top of the page, clicking a link to an anchor causes the browser to scroll the target element to the very top of the viewport. As a result, the content I want to show ends up hidden behind the fixed header.

The header is 25px tall, and I need the anchor target to appear slightly lower so it is fully visible after scrolling.

I would prefer a solution using HTML or CSS, but a JavaScript solution would also be acceptable.

Short Answer

By the end of this page, you will understand why fixed headers can cover anchor targets, and how to fix that using modern CSS such as scroll-margin-top, older CSS workarounds, and JavaScript when needed.

Concept

In-page anchors let users jump to a specific part of a page using a URL fragment such as #contact or #pricing.

For example:

<a href="#contact">Go to Contact</a>

<section id="contact">
  <h2>Contact</h2>
</section>

When the link is clicked, the browser scrolls the element with id="contact" into view. The problem is that browsers typically align that target near the top of the viewport.

If your layout has a fixed header, that header stays pinned to the top while the page scrolls. This means the browser may correctly scroll to the anchor, but the visible content can still be hidden underneath the header.

This matters in real projects because fixed navigation bars are common in:

  • documentation sites
  • landing pages
  • dashboards
  • single-page websites
  • long articles with table-of-contents links

A good anchor offset improves usability, accessibility, and navigation. Users should land on the section heading itself, not on a position where the heading is blocked.

Mental Model

Think of an anchor jump like placing a sticky note at the top edge of a window.

The browser says, "Put this section at the top."

But your fixed header is like a strip of cardboard taped across the top of that window. The section still reaches the top, but part of it is hidden behind the cardboard.

Offsetting the anchor means telling the browser:

"Stop scrolling a little earlier so the section appears just below the fixed header."

So instead of aiming for the absolute top of the page, you aim for top minus header height.

Take Quiz

Syntax and Examples

The best modern CSS solution is usually scroll-margin-top.

Modern CSS solution

<nav class="header">
  <a href="#about">About</a>
</nav>

<section id="about" class="anchor-section">
  <h2>About</h2>
  <p>This section will not hide behind the fixed header.</p>
</section>
.header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 25px;
}

.anchor-section {
  scroll-margin-top: 25px;
}

What this does

  • position: fixed keeps the header at the top.
  • scroll-margin-top: 25px tells the browser to leave extra space above the target when scrolling to it.

Apply it to headings directly

If your anchors are on headings, you can do this:

<h2 id="features">Features</h2>
h2 {
  scroll-margin-top: 25px;
}

Older CSS workaround

Before scroll-margin-top was widely used, a common trick was to create invisible space above the target.

<section id="services" class="offset-anchor">
  <h2>Services</h2>
</section>
.offset-anchor {
  padding-top: 25px;
  margin-top: -25px;
}

How this workaround behaves

  • padding-top creates extra space inside the target
  • margin-top: -25px pulls the element back up visually
  • the browser still scrolls to the element's top position, which now effectively includes the offset

JavaScript option

If you need full control, you can scroll manually.

<a href="#contact" class="jump-link">Contact</a>

<section id="contact">
  <h2>Contact</h2>
</section>
document.querySelectorAll('.jump-link').forEach(link => {
  link.addEventListener('click', event => {
    event.preventDefault();

    const targetId = link.getAttribute('href');
    const target = document.querySelector(targetId);
    const headerOffset = 25;
    const elementTop = target.getBoundingClientRect().top + window.pageYOffset;
    const offsetPosition = elementTop - headerOffset;

    window.scrollTo({
      top: offsetPosition,
      behavior: 'smooth'
    });
  });
});

This is useful when the header height changes or when you want animated scrolling.

Step by Step Execution

Consider this example:

<a href="#team">Team</a>

<header class="header">My Header</header>

<section id="team" class="anchor-section">
  <h2>Team</h2>
  <p>Meet the team.</p>
</section>
.header {
  position: fixed;
  top: 0;
  height: 25px;
}

.anchor-section {
  scroll-margin-top: 25px;
}

Here is what happens step by step:

  1. The user clicks the link with href="#team".
  2. The browser looks for the element with id="team".
  3. The browser starts scrolling that element into view.
  4. Because .anchor-section has scroll-margin-top: 25px, the browser leaves 25px of space above it.
  5. The fixed header stays at the top of the page.
  6. The Team heading appears below the header instead of behind it.

Without scroll-margin-top, step 4 would not happen, and the section could be hidden.

Real World Use Cases

This pattern appears in many common interfaces.

Documentation pages

A table of contents links to sections like #installation or #api, while a fixed top navigation bar remains visible.

Marketing or landing pages

Buttons like "Features", "Pricing", and "Contact" jump to sections on the same page.

Admin dashboards

A fixed app header or toolbar can overlap section headings when navigating within long settings pages.

Help centers and FAQs

Users click question links and should land with the full heading visible.

Single-page portfolios

Navigation links jump to About, Projects, and Contact sections while the header stays fixed for easy navigation.

Take Quiz

Real Codebase Usage

In real projects, developers usually solve this in one of these ways:

1. Use CSS on anchor targets

A common pattern is to apply scroll-margin-top to headings or sections that may be linked to.

section[id],
h2[id],
h3[id] {
  scroll-margin-top: 64px;
}

This works well in documentation sites and component libraries.

2. Use a CSS custom property for header height

This makes maintenance easier.

:root {
  --header-height: 25px;
}

.header {
  height: var(--header-height);
}

[id] {
  scroll-margin-top: var(--header-height);
}

If the header height changes, you update it in one place.

3. Combine with smooth scrolling

html {
  scroll-behavior: smooth;
}

This improves the user experience without JavaScript.

4. Use JavaScript for dynamic layouts

If your header height changes between mobile and desktop, developers sometimes measure it directly.

const header = document.querySelector('.header');
const headerHeight = header.offsetHeight;

Then they subtract that value during scrolling.

5. Add guard clauses in JavaScript

In production code, always check that the target exists.

const target = document.querySelector(targetId);
if (!target) return;

This avoids runtime errors when a link points to a missing element.

Common Mistakes

1. Forgetting that the header height may change

A hard-coded 25px works only if the header is always exactly 25px tall.

Better:

:root {
  --header-height: 25px;
}

[id] {
  scroll-margin-top: var(--header-height);
}

2. Applying the offset to the wrong element

Broken approach:

.header {
  scroll-margin-top: 25px;
}

This does nothing for anchor targets because the offset belongs on the element being scrolled to, not on the fixed header.

3. Using extra empty elements unnecessarily

Older solutions often inserted hidden anchor elements just to create spacing. This works, but it makes HTML harder to maintain.

Prefer modern CSS when possible.

4. Forgetting browser behavior with JavaScript links

Broken code:

link.addEventListener('click', () => {
  window.scrollTo({ top: 100 });
});

This ignores the actual anchor target and may still allow the browser's default anchor jump.

Better:

link.addEventListener('click', event => {
  event.preventDefault();
});

5. Hiding content with negative margins without testing

This workaround can affect layout if used carelessly.

.offset-anchor {
  padding-top: 25px;
  margin-top: -25px;
}

It can work, but test it with surrounding elements.

6. Not accounting for responsive headers

A mobile header might be taller than a desktop header. If so, use a CSS variable or JavaScript measurement instead of a single fixed value.

Comparisons

ApproachBest forProsCons
scroll-margin-topModern browsers and clean CSS solutionsSimple, readable, no extra markupDepends on browser support in older environments
padding-top + negative margin-topOlder CSS workaroundNo JavaScript neededCan affect layout and is less intuitive
JavaScript scrollTo()Dynamic headers or custom behaviorFull control, can support smooth scrolling and dynamic offsetsMore code, more maintenance
Empty anchor spacer elementLegacy pagesWorks in simple casesAdds unnecessary markup

CSS vs JavaScript

OptionWhen to choose it
CSSWhen the header height is known and stable
JavaScriptWhen the offset depends on runtime conditions, such as responsive or shrinking headers

scroll-margin-top vs scroll-padding-top

PropertyApplies toUse case
scroll-margin-topThe target elementBest when specific elements are linked to
scroll-padding-topThe scrolling containerUseful when controlling how scrolling behaves inside a container or for the whole page in some layouts
Take Quiz

Cheat Sheet

/* Fixed header */
.header {
  position: fixed;
  top: 0;
  height: 25px;
}

/* Best modern fix */
[id] {
  scroll-margin-top: 25px;
}

/* Optional smooth scrolling */
html {
  scroll-behavior: smooth;
}
/* Reusable with a variable */
:root {
  --header-height: 25px;
}

[id] {
  scroll-margin-top: var(--header-height);
}
/* Older workaround */
.offset-anchor {
  padding-top: 25px;
  margin-top: -25px;
}
/* JavaScript manual offset */
const headerOffset = 25;
const elementTop = target.getBoundingClientRect().top + window.pageYOffset;
window.scrollTo({ top: elementTop - headerOffset, behavior: 'smooth' });

Rules to remember

  • Put the offset on the target element, not the header.
  • Use CSS first if possible.
  • Use JavaScript when the header height is dynamic.
  • Test on mobile if your header changes size.
  • scroll-margin-top is the cleanest modern solution.

FAQ

How do I stop a fixed header from covering anchor links?

Use scroll-margin-top on the target element, with a value equal to the fixed header height.

Is there a pure CSS way to offset anchor scrolling?

Yes. The preferred modern solution is scroll-margin-top. An older workaround is padding-top with a matching negative margin-top.

Should I use JavaScript to offset anchors?

Only if CSS is not enough, such as when the header height changes dynamically or you need custom scrolling behavior.

What is the difference between scroll-margin-top and margin-top?

margin-top changes layout spacing. scroll-margin-top changes where the browser positions an element during scrolling.

Can I apply anchor offset to all elements with IDs?

Yes, many projects use:

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

But make sure that broad rule does not affect elements you do not want to target.

Does smooth scrolling fix the overlap problem?

No. Smooth scrolling changes the animation, not the final position. You still need an offset solution.

What if my fixed header has different heights on mobile and desktop?

Use responsive CSS variables or JavaScript to calculate the current header height.

Take Quiz

Mini Project

Description

Build a simple single-page site with a fixed header and navigation links that jump to sections such as About, Services, and Contact. The project demonstrates how to make anchor navigation work correctly without hiding section headings behind the fixed header.

Goal

Create a page where in-page navigation links scroll to visible section headings even when a fixed header remains at the top.

Requirements

  1. Create a fixed header with at least three navigation links.
  2. Add at least three sections that can be reached with anchor links.
  3. Prevent section headings from being hidden behind the fixed header.
  4. Add smooth scrolling for a better user experience.
  5. Keep the solution in HTML and CSS only.
Take Quiz

Keep learning

Related questions

View more