A header that stays anchored to the top of the viewport as users scroll is one of the most reliable UX improvements you can make to any site. It keeps navigation permanently accessible, reduces the effort required to move between sections, and gives a polished, professional feel that visitors notice immediately. Whether you are building from scratch or customising a Canvas HTML Template, the implementation follows the same core principles — and this guide covers every approach, from a pure CSS one-liner to a JavaScript-driven shrinking navbar with scroll-triggered classes.
Key Takeaways
- Bootstrap 5’s built-in
.sticky-toputility applies a CSS sticky header with no JavaScript required. .fixed-topremoves the element from document flow, so you must compensate with body padding.- A scroll-triggered class lets you shrink, recolour, or add a shadow to the navbar after the user passes a threshold.
- CSS custom properties (
--cnvs-themecolorin Canvas) make dynamic colour changes trivial to manage. - Smooth-scroll anchor links require an offset calculation when a fixed header is present.
- Performance matters: use
will-change: transformandposition: stickywherever possible to keep sticky headers paint-efficient.
Understanding sticky-top vs fixed-top in Bootstrap 5
Bootstrap 5 ships with two utility classes that both pin a navbar to the top of the screen, but they behave very differently at the CSS level.
.sticky-topsetsposition: sticky; top: 0. The element remains in normal document flow until the scroll position reaches it, then it sticks. No extra body padding is needed..fixed-topsetsposition: fixed; top: 0. The element is removed from document flow immediately, which means content underneath slides behind it — you must manually addpadding-topto<body>or the first section equal to the navbar’s height.
For most projects, .sticky-top is the simpler starting point. Use .fixed-top when you need the header to be visible before the user has scrolled at all — for instance, when your hero section has a full-viewport background image and you want the navbar to float over it.
<!-- Sticky (stays in flow) -->
<header class="sticky-top">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<!-- nav content -->
</nav>
</header>
<!-- Fixed (out of flow — compensate with body padding) -->
<header class="fixed-top">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<!-- nav content -->
</nav>
</header>

Compensating for the Fixed Header Height
When you use .fixed-top, the first thing users see is content lurking beneath the navbar. The cleanest fix uses a CSS custom property so the offset stays in sync if the header height ever changes.
<style>
:root {
--navbar-height: 72px;
}
body {
padding-top: var(--navbar-height);
}
/ If hero sections need a full-bleed treatment, reset for those /
.hero-fullscreen {
margin-top: calc(-1 * var(--navbar-height));
}
</style>
If the navbar height is genuinely dynamic (it collapses on mobile, or shrinks on scroll), set the variable via JavaScript after the DOM is ready:
<script>
document.addEventListener('DOMContentLoaded', function () {
const navbar = document.getElementById('mainNav');
document.documentElement.style.setProperty(
'--navbar-height',
navbar.offsetHeight + 'px'
);
});
</script>
Adding a Scroll-Triggered Shrink Effect
A navbar that slims down after the user scrolls past a threshold is a popular pattern — it gives more visual space to the hero area while keeping navigation accessible. The technique relies on a small scroll listener that toggles a CSS class.
<style>
#mainNav {
transition: padding 0.3s ease, background-color 0.3s ease, box-shadow 0.3s ease;
padding-top: 1.25rem;
padding-bottom: 1.25rem;
}
#mainNav.navbar-shrink {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
background-color: #ffffff !important;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
</style>
<script>
(function () {
const nav = document.getElementById('mainNav');
const SHRINK_THRESHOLD = 80; // pixels
function handleScroll() {
if (window.scrollY > SHRINK_THRESHOLD) {
nav.classList.add('navbar-shrink');
} else {
nav.classList.remove('navbar-shrink');
}
}
window.addEventListener('scroll', handleScroll, { passive: true });
handleScroll(); // run once on load
})();
</script>
The { passive: true } flag is important — it tells the browser the listener will never call preventDefault(), allowing it to optimise scroll performance significantly.

Implementing a Sticky Header in Canvas Template
The Canvas HTML Template includes several built-in header styles that already handle sticky behaviour through data attributes. The most direct approach is the data-sticky-header attribute, which Canvas’s functions.bundle.js watches automatically.
<!-- Canvas sticky header via data attribute -->
<header id="header" class="header-size-sm">
<div id="header-wrap" data-sticky-header="true">
<nav class="navbar navbar-expand-lg">
<!-- Canvas nav markup -->
</nav>
</div>
</header>
Canvas also exposes its theme colour through the CSS custom property --cnvs-themecolor, so you can reference it when you recolour the navbar on scroll without hard-coding a hex value:
<style>
#header-wrap.sticky-header-active {
background-color: var(--cnvs-themecolor);
}
</style>
If you are building a page from scratch with Canvas rather than modifying an existing demo, the Canvas Builder workflow guide explains how to assemble header and section blocks before applying these customisations.
Fixing Anchor Link Offsets with a Sticky Header
One of the most common side-effects of a fixed or sticky header is that anchor links jump to a position hidden behind the navbar. The modern CSS fix requires no JavaScript at all:
<style>
/ Applies to every element targeted by an anchor link /
:target {
scroll-margin-top: var(--navbar-height, 80px);
}
</style>
scroll-margin-top is supported in all modern browsers and is considerably cleaner than the older technique of adding invisible padding and negative margin to every section. For JavaScript-driven smooth scroll, pass an offset to the calculation:
<script>
document.querySelectorAll('a[href^="#"]').forEach(function (anchor) {
anchor.addEventListener('click', function (e) {
const target = document.querySelector(this.getAttribute('href'));
if (!target) return;
e.preventDefault();
const navbarHeight = document.getElementById('mainNav').offsetHeight;
const targetTop = target.getBoundingClientRect().top + window.scrollY - navbarHeight;
window.scrollTo({ top: targetTop, behavior: 'smooth' });
});
});
</script>
This pairs naturally with a well-structured hero section. If you are looking for hero layout inspiration, the 12 Bootstrap 5 hero section designs guide is a strong companion read.
Performance and Accessibility Considerations
A sticky header that performs poorly can cause jank on lower-end devices. Apply these practices consistently:
- Prefer
position: stickyoverposition: fixedwhere possible. Sticky elements do not trigger layout recalculation on every scroll tick. - Add
will-change: transformto the sticky element if you are animating it, but remove the property once the animation ends to free GPU memory. - Use
{ passive: true }on all scroll event listeners, as shown above. - Avoid querying the DOM inside the scroll handler. Cache all element references outside the function.
- Ensure keyboard focus remains visible beneath the header. Test tab order and adjust
z-indexif focused elements disappear behind the navbar. - Set
aria-label="Main navigation"on your<nav>element so screen readers announce it correctly regardless of visual position.
For a deeper look at how Bootstrap 5 compares with other frameworks when it comes to these kinds of UI patterns, the 15 Bootstrap 5 UI patterns post covers many of the building blocks that complement a sticky header.
Frequently Asked Questions
A sticky header (.sticky-top) uses position: sticky and stays within the document flow until the user scrolls to it. A fixed header (.fixed-top) uses position: fixed and is immediately removed from flow, overlapping content below it. Sticky is generally simpler because it does not require body padding compensation.
Add scroll-margin-top to targeted elements in your CSS, set to the same value as your navbar height. For example: :target { scroll-margin-top: 80px; }. This is a native CSS solution with no JavaScript dependency and full modern-browser support.
Yes. Start the navbar with a transparent background and use a scroll listener to add a class once the user passes a threshold. Define the background colour, shadow, and any size changes in that class using CSS transitions so the change animates smoothly. The shrink effect example in this article demonstrates exactly this pattern.
Yes. Canvas includes multiple header styles with built-in sticky behaviour controlled via a data-sticky-header attribute on the header wrapper element. The template’s functions.bundle.js handles all scroll detection automatically, so no custom JavaScript is required unless you want to extend the default behaviour.
A well-implemented sticky header has negligible SEO impact. For performance, avoid layout-triggering properties (like changing height or width inside a scroll handler) and prefer position: sticky over position: fixed where the design allows. Using { passive: true } scroll listeners and CSS transitions instead of JavaScript animation will keep your Cumulative Layout Shift and Interaction to Next Paint scores healthy.
Looking for a production-ready Bootstrap 5 HTML template? Browse Canvas Template demos and find the perfect starting point for your next project.
If you’re building with the Canvas HTML Template and want to ship production-ready Bootstrap 5 layouts faster, try Canvas Builder free — the visual builder that exports clean Canvas-ready markup in minutes.
Skip the setup — build it free
Spin up a complete Bootstrap 5 site, blog included, with Canvas Builder. No coding, no cost.
Canvas Team
Tutorials and tips for building beautiful Bootstrap 5 websites with the Canvas HTML Template and Canvas Builder.
More from the Canvas Blog