Sticky Header That Doesn't Cover Your Content
Reproduce an anchor hidden beneath a sticky header and keep a measured scroll offset correct when the header wraps.
A sticky header should not hide the section someone just selected. Check the jump destination as well as the header's position. A hard-coded offset that works on desktop can become too small when the navigation wraps on a phone.
This example measures its header and gives the root scroll container matching top padding for scroll operations. Save it as sticky-anchors.html and open it. The long sections are there to provide enough scrolling space for the experiment.
Run a header with real destinations
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Keep anchor headings below a sticky header</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
padding: 16px;
font: 1rem/1.6 system-ui, sans-serif;
color: #080b0e;
background: #ffffff;
}
main { max-width: 60rem; margin-inline: auto; }
body { padding: 0; }
html { scroll-padding-top: var(--header-offset, 6rem); }
.site-header {
position: sticky;
top: 0;
z-index: 10;
padding: 12px 16px;
background: #ffffff;
border-bottom: 1px solid #dce3ed;
}
nav { display: flex; flex-wrap: wrap; gap: 12px; }
a { color: #034bff; }
a:focus-visible { outline: 3px solid #034bff; outline-offset: 3px; }
main { padding: 16px; }
section { min-height: 100vh; padding-block: 16px; }
h2 { margin-top: 0; }
</style>
</head>
<body>
<header class="site-header">
<strong>Delivery guide</strong>
<nav aria-label="Page sections">
<a href="#overview">Overview</a>
<a href="#shipping">Shipping</a>
<a href="#returns">Returns</a>
</nav>
</header>
<main>
<h1>Follow an anchor without losing the heading</h1>
<section id="overview"><h2>Overview</h2><p>Start with the order details.</p></section>
<section id="shipping"><h2>Shipping</h2><p>Check the destination and chosen delivery method.</p></section>
<section id="returns"><h2>Returns</h2><p>Keep the order reference when asking a question.</p></section>
</main>
<script>
const header = document.querySelector('.site-header');
function updateOffset() {
const offset = header.getBoundingClientRect().height + 16;
document.documentElement.style.setProperty('--header-offset', offset + 'px');
}
new ResizeObserver(updateOffset).observe(header, { box: 'border-box' });
updateOffset();
</script>
</body>
</html>
Choose Shipping. The Shipping heading should stop below the header with a small visible gap. The header retains its place in normal flow before becoming stuck to the top, so the opening content does not need the compensating body padding usually required by a fixed header.
Reproduce the covered heading
In DevTools, temporarily change the root rule to scroll-padding-top: 0. Choose Returns and then Shipping again. The destination can now align with the top of the viewport underneath the sticky header.
Restore the original rule and repeat the jump. It should leave room for the header again. The target is the section, whose heading has no top margin in this example; an extra heading margin could otherwise make the measurement look different.
Keep the offset tied to the header
The script measures the header's rendered height and adds 16px of breathing room. A ResizeObserver updates that value if the header changes size. This covers wrapping or a font change without guessing a second mobile height.
If your header has a genuinely fixed, known size at each layout, a CSS value at each breakpoint may be enough. If it changes with translated labels or dynamic content, a measured value avoids assuming that every visitor sees the same height.
Scroll padding affects scroll placement; it does not add empty content above every section. If your design scrolls a nested element rather than the document, configure that actual scroll container. An offset on html will not automatically repair every independent scrolling region.
Check the sticky ancestor chain
If the header never sticks, confirm its non-auto top inset and enough scrolling distance. Then inspect ancestor overflow. A scrolling ancestor can change the region to which sticky positioning relates. A header inside a very short parent also has little room to travel.
The z-index here places this header above ordinary page content in this simple layout. The number is not a universal guarantee over every other stacking context. Keep the background opaque so the content passing behind it does not reduce readability.
Test navigation with the keyboard
- Reload and Tab to the navigation links. The focus outline should be visible.
- Activate Shipping with Enter. Confirm its heading remains visible below the bar.
- Narrow the viewport, then enlarge the header's text or add a longer navigation label.
- Repeat the anchor jumps after the header changes height.
This example uses normal anchor navigation and no smooth-scroll animation. It does not move keyboard focus through a custom script. If you add a custom scrolling behavior later, test focus and reduced-motion preferences as separate concerns.
Change the header height
Increase the header's vertical padding from 12px to 24px without editing the script.
Expected result: the observed height and root offset grow. Shipping still appears below the header after another jump. If it disappears again, inspect the actual root scroll-padding value and whether your observer is measuring the correct element.
For the difference between in-flow positioning and overlays, continue to relative and absolute positioning.
About
Reproduce an anchor hidden beneath a sticky header and keep a measured scroll offset correct when the header wraps.
- Responsible
- Christopher R
- Last Update
- 08/26/2026
- Reading Time
- 5 min
Reviews
Sign in to write a reviewComments (0)
Sign in to commentNo comments yet.