Question

How to Offset HTML Anchors for a Fixed Header Using CSS and JavaScript

htmljavascriptcssanchoroffset

Question

I have a fixed header at the top of my page. When I click a link to an anchor elsewhere on the same page, the browser scrolls that anchor to the very top of the viewport. Because the header is fixed, it covers the top part of the target content.

How can I offset the anchor position by about 25px, which is the height of the header, so the content remains visible after the jump?

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 overlap in-page anchors, how modern CSS solves it with scroll-margin-top, and what fallback techniques can be used when you need broader support or more control.

Concept

When you click an in-page anchor link such as #section2, the browser scrolls the target element into view. By default, it tries to align that target near the top of the viewport.

This becomes a problem when your layout includes a fixed header. A fixed header stays visible at the top of the screen, so the browser may place the target under that header instead of below it.

This is not really a bug in anchors. It is a layout issue caused by combining:

  • in-page navigation
  • fixed positioning
  • automatic browser scrolling

Why this matters in real projects:

  • Documentation pages often use anchor links.
  • Landing pages use navigation links like #features or #pricing.
  • Dashboards and admin panels often have sticky or fixed top bars.
  • Accessibility and usability improve when the correct content is visible after navigation.

The most modern solution is CSS:

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

This tells the browser to leave extra space above the element when it is scrolled into view.

Older techniques include:

  • adding top padding and negative margin
  • using a ::before pseudo-element as invisible spacing
  • JavaScript scrolling with a custom offset

In most modern codebases, CSS-first is the best approach because it is simpler, cleaner, and easier to maintain.

Mental Model

Imagine the browser is trying to place a bookmark exactly at the top edge of a window.

Now imagine you taped a solid bar across the top of that window. The bookmark is technically at the top, but you cannot see it because the bar covers it.

Offsetting an anchor means telling the browser:

"Scroll to this section, but stop a little earlier so it appears just below the fixed header."

So instead of aiming for the absolute top, you leave a small safety gap equal to the header height.

Take Quiz

Syntax and Examples

Modern CSS solution: scroll-margin-top

If the element being linked to is the anchor target, give it a top scroll margin.

<nav>
  <a href="#about">About</a>
  <a href="#contact">Contact</a>
</nav>

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

<section id="about" class="anchor-target">About section</section>
<section id="contact" class="anchor-target">Contact section</section>
.site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 25px;
}

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

How it works

  • Clicking href="#about" scrolls to the element with id="about".
  • scroll-margin-top: 25px tells the browser to leave 25px of extra space above that element.
  • The header no longer covers the content.

Using a reusable CSS variable

This is common in real projects.

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

.site-header {
  position: fixed;
  height: var(--header-height);
}

.anchor-target {
  scroll-margin-top: var(--header-height);
}

This makes it easier to update the header size in one place.

Older CSS fallback using ::before

<h2 id="features" class="offset-anchor">Features</h2>
.offset-anchor::before {
  content: "";
  display: block;
  height: 25px;
  margin-top: -25px;
}

Why this works

The invisible pseudo-element creates space before the heading, and the negative margin pulls the layout back so the page does not visibly shift.

JavaScript solution

If you need custom scrolling behavior:

<a href="#pricing" class="jump-link">Pricing</a>
<section id="pricing">Pricing section</section>
document.querySelectorAll('.jump-link').forEach(link => {
  link.addEventListener('click', event => {
    event.preventDefault();

    const targetId = link.getAttribute('href');
    const target = document.querySelector(targetId);
    const offset = 25;

    if (target) {
      const top = target.getBoundingClientRect().top + window.pageYOffset - offset;
      window.scrollTo({ top, behavior: 'smooth' });
    }
  });
});

This manually scrolls to the target position minus the header height.

Step by Step Execution

Consider this example:

<a href="#section1">Go to Section 1</a>

<section id="section1" class="anchor-target">Section 1</section>
.anchor-target {
  scroll-margin-top: 25px;
}

Here is what happens step by step:

  1. The user clicks the link Go to Section 1.
  2. The browser reads href="#section1".
  3. It looks for an element with id="section1".
  4. It prepares to scroll that element into view.
  5. Because the target has scroll-margin-top: 25px, the browser leaves 25px of space above it.
  6. The section appears slightly below the top of the viewport.
  7. The fixed header no longer hides the section title or content.

JavaScript trace example

const target = document.querySelector('#section1');
const offset = 25;
const top = target.getBoundingClientRect().top + window.pageYOffset - offset;
window.scrollTo({ top });

Step by step:

  1. document.querySelector('#section1') finds the target element.
  2. getBoundingClientRect().top gets the target's position relative to the current viewport.
  3. window.pageYOffset adds the amount already scrolled.
  4. Subtracting offset moves the final position up by 25px.
  5. window.scrollTo({ top }) scrolls the page to that adjusted position.

Real World Use Cases

Common places this problem appears

  • Single-page websites where navbar links jump to sections like About, Services, and Contact.
  • Documentation sites with a table of contents linking to headings.
  • FAQ pages where users jump directly to a question.
  • Admin dashboards with sticky top navigation.
  • Blog posts that link to headings inside the article.

Example scenarios

Product landing page

A fixed navigation bar links to #features, #testimonials, and #pricing. Without offsetting, users click a link and land on a heading that is partially hidden.

API documentation

A sidebar links to methods and endpoints. Developers need the heading and example code to be visible immediately.

Long settings page

A menu jumps to #security, #notifications, and #billing. Proper anchor offsets make navigation feel polished and predictable.

Take Quiz

Real Codebase Usage

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

1. Set a shared header height variable

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

.section-anchor {
  scroll-margin-top: var(--header-height);
}

This avoids hardcoding the same number in multiple places.

2. Apply the offset only to navigable sections

Instead of applying it to every element, teams often add a class only to headings or sections that can be linked directly.

<h2 id="installation" class="section-anchor">Installation</h2>
<h2 id="usage" class="section-anchor">Usage</h2>

3. Combine with smooth scrolling

html {
  scroll-behavior: smooth;
}

This improves the user experience for anchor jumps.

4. Use JavaScript when the header height changes dynamically

For example:

  • mobile menu expands
  • header shrinks on scroll
  • layout changes across breakpoints

In those cases, developers may measure the header height dynamically before scrolling.

5. Use guard checks in JavaScript

A common safe pattern:

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

This prevents errors if a link points to a missing element.

6. Keep anchor targets semantic

Developers often use real section headings with id attributes instead of empty anchor elements. This improves readability and accessibility.

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

Common Mistakes

1. Offsetting the wrong element

Beginners sometimes style the link instead of the target.

Broken

a {
  scroll-margin-top: 25px;
}

This usually does not help, because the browser scrolls to the element with the matching id, not the clicked link.

Correct

#contact {
  scroll-margin-top: 25px;
}

2. Forgetting that the header height may change

If your header is 25px on desktop but 60px on mobile, a fixed value may be wrong on some screens.

Better

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

@media (min-width: 768px) {
  :root {
    --header-height: 25px;
  }
}

3. Using extra empty elements unnecessarily

Older code often adds empty anchor tags only for positioning. This can clutter HTML.

Less ideal

<a id="about"></a>
<h2>About</h2>

Better

<h2 id="about">About</h2>

4. Using JavaScript when CSS already solves it

JavaScript works, but it adds complexity. Start with CSS unless you need custom behavior.

5. Forgetting browser support constraints in older projects

If you maintain an older codebase, scroll-margin-top may not be the chosen approach. In that case, the ::before method or JS may be more appropriate.

6. Breaking layout with padding hacks

A common trick is adding top padding to the target. This can create visible spacing that affects the normal layout.

#about {
  padding-top: 25px;
}

This may work visually, but it changes the element's layout, not just its scroll position.

Comparisons

ApproachBest forProsCons
scroll-margin-topModern browsers and clean CSS solutionsSimple, readable, no extra markupMay not match legacy support requirements
::before offset hackOlder CSS-based projectsNo JavaScript required, works in many casesLess intuitive, more of a workaround
Padding + negative marginLegacy layoutsCan work without JSCan affect layout and be harder to maintain
JavaScript scrollToDynamic headers or custom behaviorFully controllable, supports smooth scrolling logicMore code, more maintenance

scroll-margin-top vs JavaScript

Featurescroll-margin-topJavaScript
SimplicityVery simpleMore complex
PerformanceGreatUsually fine, but more work
Dynamic header heightLimited unless combined with variablesEasy to calculate dynamically
MaintenanceEasierHarder
Recommended first choiceYesOnly when needed

Fixed header vs sticky header

Header typeBehaviorAnchor issue possible?
position: fixedAlways stays in viewportYes
position: stickySticks only after scrolling to a thresholdSometimes

If the element can cover the top of the viewport, anchor offsetting may still be useful.

Take Quiz

Cheat Sheet

/* Best modern approach */
.target {
  scroll-margin-top: 25px;
}

/* Reusable variable */
:root {
  --header-height: 25px;
}

.target {
  scroll-margin-top: var(--header-height);
}

/* Smooth scrolling */
html {
  scroll-behavior: smooth;
}

/* Older fallback */
.target::before {
  content: "";
  display: block;
  height: 25px;
  margin-top: -25px;
}
// JavaScript offset scroll
const target = document.querySelector('#section');
const offset = 25;
const top = target.getBoundingClientRect().top + window.pageYOffset - offset;
window.scrollTo({ top, behavior: 'smooth' });

Rules to remember

  • Put the id on the element you want to scroll to.
  • Apply offset styling to the target element, not the link.
  • Use scroll-margin-top first in modern projects.
  • Use CSS variables if the header height is reused.
  • Use JavaScript only when you need dynamic control.

Common target pattern

<h2 id="faq" class="target">FAQ</h2>

Edge cases

  • Responsive headers may need different offsets.
  • Shrinking headers may require JavaScript.
  • Old codebases may still use pseudo-element hacks.

FAQ

Why do fixed headers overlap anchor links?

Because the browser scrolls the target to the top of the viewport, and the fixed header sits on top of that area.

What is the best CSS way to offset an anchor?

Use scroll-margin-top on the target element. It is the cleanest modern solution.

Can I solve fixed header anchor issues without JavaScript?

Yes. In most modern projects, CSS alone is enough using scroll-margin-top or an older pseudo-element fallback.

Should I put the id on an empty anchor tag or on the heading itself?

Usually on the heading or section itself. It keeps the HTML cleaner and more semantic.

What if my header height changes on mobile?

Use a CSS variable and update it in media queries, or use JavaScript if the height changes dynamically at runtime.

Does scroll-behavior: smooth fix the overlap problem?

No. It only changes the animation of scrolling. You still need an offset solution.

When should I use JavaScript for anchor offsets?

Use it when the header height is dynamic or when you need custom scrolling behavior beyond what CSS provides.

Take Quiz

Mini Project

Description

Build a simple one-page site with a fixed header and navigation links that jump to sections such as About, Services, and Contact. The project demonstrates how to prevent anchored sections from being hidden behind the fixed header.

Goal

Create in-page navigation that scrolls to each section while keeping the section heading visible below a fixed header.

Requirements

  1. Create a fixed header at the top of the page.
  2. Add at least three navigation links that point to section IDs.
  3. Make each section target visible below the header after navigation.
  4. Use a CSS-first solution.
  5. Add smooth scrolling for a better user experience.
Take Quiz

Keep learning

Related questions

View more