Question
How to Offset HTML Anchors for a Fixed Header Using CSS and JavaScript
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
#featuresor#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
::beforepseudo-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.
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 withid="about". scroll-margin-top: 25pxtells the browser to leave25pxof 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:
- The user clicks the link
Go to Section 1. - The browser reads
href="#section1". - It looks for an element with
id="section1". - It prepares to scroll that element into view.
- Because the target has
scroll-margin-top: 25px, the browser leaves25pxof space above it. - The section appears slightly below the top of the viewport.
- 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:
document.querySelector('#section1')finds the target element.getBoundingClientRect().topgets the target's position relative to the current viewport.window.pageYOffsetadds the amount already scrolled.- Subtracting
offsetmoves the final position up by25px. 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.
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
| Approach | Best for | Pros | Cons |
|---|---|---|---|
scroll-margin-top | Modern browsers and clean CSS solutions | Simple, readable, no extra markup | May not match legacy support requirements |
::before offset hack | Older CSS-based projects | No JavaScript required, works in many cases | Less intuitive, more of a workaround |
| Padding + negative margin | Legacy layouts | Can work without JS | Can affect layout and be harder to maintain |
JavaScript scrollTo | Dynamic headers or custom behavior | Fully controllable, supports smooth scrolling logic | More code, more maintenance |
scroll-margin-top vs JavaScript
| Feature | scroll-margin-top | JavaScript |
|---|---|---|
| Simplicity | Very simple | More complex |
| Performance | Great | Usually fine, but more work |
| Dynamic header height | Limited unless combined with variables | Easy to calculate dynamically |
| Maintenance | Easier | Harder |
| Recommended first choice | Yes | Only when needed |
Fixed header vs sticky header
| Header type | Behavior | Anchor issue possible? |
|---|---|---|
position: fixed | Always stays in viewport | Yes |
position: sticky | Sticks only after scrolling to a threshold | Sometimes |
If the element can cover the top of the viewport, anchor offsetting may still be useful.
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
idon the element you want to scroll to. - Apply offset styling to the target element, not the link.
- Use
scroll-margin-topfirst 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.
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
- Create a fixed header at the top of the page.
- Add at least three navigation links that point to section IDs.
- Make each section target visible below the header after navigation.
- Use a CSS-first solution.
- Add smooth scrolling for a better user experience.
Keep learning
Related questions
Allow Only Numeric Input (0-9) in HTML Input Using jQuery
Learn how to allow only digits 0-9 in an HTML input using jQuery, with examples, validation tips, common mistakes, and best practices.
Apply CSS !important with jQuery .css()
Learn why jQuery .css() cannot set !important and how to use CSSStyleDeclaration.setProperty() to apply inline important styles safely.
CSS ::before and ::after on Input Elements
Learn why CSS ::before and ::after usually do not work on input elements, what replaced elements are, and practical alternatives.