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.
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: fixedkeeps the header at the top.scroll-margin-top: 25pxtells 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-topcreates extra space inside the targetmargin-top: -25pxpulls 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:
- The user clicks the link with
href="#team". - The browser looks for the element with
id="team". - The browser starts scrolling that element into view.
- Because
.anchor-sectionhasscroll-margin-top: 25px, the browser leaves 25px of space above it. - The fixed header stays at the top of the page.
- The
Teamheading 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.
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
| Approach | Best for | Pros | Cons |
|---|---|---|---|
scroll-margin-top | Modern browsers and clean CSS solutions | Simple, readable, no extra markup | Depends on browser support in older environments |
padding-top + negative margin-top | Older CSS workaround | No JavaScript needed | Can affect layout and is less intuitive |
JavaScript scrollTo() | Dynamic headers or custom behavior | Full control, can support smooth scrolling and dynamic offsets | More code, more maintenance |
| Empty anchor spacer element | Legacy pages | Works in simple cases | Adds unnecessary markup |
CSS vs JavaScript
| Option | When to choose it |
|---|---|
| CSS | When the header height is known and stable |
| JavaScript | When the offset depends on runtime conditions, such as responsive or shrinking headers |
scroll-margin-top vs scroll-padding-top
| Property | Applies to | Use case |
|---|---|---|
scroll-margin-top | The target element | Best when specific elements are linked to |
scroll-padding-top | The scrolling container | Useful when controlling how scrolling behaves inside a container or for the whole page in some layouts |
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-topis 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.
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
- Create a fixed header with at least three navigation links.
- Add at least three sections that can be reached with anchor links.
- Prevent section headings from being hidden behind the fixed header.
- Add smooth scrolling for a better user experience.
- Keep the solution in HTML and CSS only.
Keep learning
Related questions
Angular ngClass Conditional Class Binding Explained
Learn how to use Angular ngClass for conditional classes, fix common binding mistakes, and understand why this template error happens.
CSS :not() Selector for Elements Without a Class or Attribute
Learn how to use the CSS :not() selector to target elements that do not have a specific class or attribute, with examples and common pitfalls.
CSS Font Scaling Relative to Container Size: %, em, rem, vw, and Responsive Text
Learn how CSS font scaling really works and how to make text responsive using %, em, rem, vw, clamp(), and media queries.