Sticky header
The everyday navigation bar of a site, held at the top of the window: a logo, a row of links, one button, in your own markup.
Documentation
-
Copy to Webflow
Add Geist and Geist Mono to your site's fonts (Site settings, Fonts, Google Fonts) before pasting: Webflow removes any font your site has not installed. -
Add custom CSS
Custom CSS in WebflowThis CSS already ships inside the Webflow embed, so a pasted section needs nothing added. Paste it yourself, in the page or the site custom code, only if the effect runs on several pages. The tutorial explains where.
See the tutorialThe bar is a box of your own: its class gives its padding, which is its height at the top of the page and what it gives up as it condenses, and its type. Do not give it a background, a transform or a position: the Embed holds it to the window and builds its ground. Give the first section of the page a padding at the top that clears the bar, and mark each dark sectiondata-sticky-header-tone="dark"in its Settings panel. Nothing above the bar may carry a transform or a filter, or it stops being held to the window. -
Add custom JS
Custom JavaScript in WebflowThis script already ships inside the Webflow embed, settings block included. You paste it yourself, before the closing body tag of the page or the site custom code, only when the effect has to run on several pages, and the tutorial says how to choose.
See the tutorial -
Tune it
How to add a custom attribute in WebflowA custom attribute is a pair, a name and a value, typed in the Settings panel of the element you selected, under Custom attributes. The name is what the script looks for, the value is what it reads. The tutorial shows the panel.
See the tutorialCustom attributesSelect the section on the canvas and open the Settings panel. Under Custom attributes, add a pair: the name on the left, the value on the right.Custom attribute What it does data-sticky-header Goes on the bar, a header as a rule, and takes no value: leave the value field empty. It holds the bar at the top of the window. The settings below go on the same element. Without it on any element of the page, the embed marks the first header of the page itself. data-sticky-header-tone="dark" On each section of the page that has a dark ground. While it is behind the bar, the text of the bar takes the colour of the ground and its ground the colour of the ink, cut on the edge of the section. A section with no mark is light; on a site set to base dark, mark the light sections data-sticky-header-tone="light" instead. Without any mark the bar keeps one tone. [ Optional ] data-sticky-header-plate On an element of the bar that is filled with the colour of the text, the button for one. It takes no value. Its ground and its text change places with the tone. Without it a filled button keeps its two colours and disappears on a dark section. [ Optional ] data-sticky-header-keep On what must keep its own colours in the bar, a logo in colour or a picture. It takes no value. It is left out of the tone and stands over the cut. [ Optional ] data-sticky-header-top On an element of the bar that only shows at the top of the page, the line beside the logo in the paste. It takes no value. It fades and lifts away as the bar condenses and keeps its place, so nothing else moves. [ Optional ] data-sticky-header-state, data-sticky-header-on, data-sticky-header-hidden, data-sticky-header-ready Written by the engine on the bar: top or scrolled, light or dark, hidden while the bar is away, ready once it is mounted. Never written by hand; the stylesheet reads them, and so can a rule of yours. [ Optional ] data-sticky-header-ground, data-sticky-header-cut, data-sticky-header-line, data-sticky-header-fill Written by the engine on the six blocks it adds to the bar: its ground, its hairline, the three plates that cut it in two tones, and the line of the reading progress. data-sticky-header-roll is written the same way on the box it puts around each label. Never written by hand, and removed by destroy(). [ Optional ] SettingsEvery setting is a custom attribute too, added the same way and on the same section. An attribute always wins over the defaults written in the code.Attribute Default What it is data-sticky-header-ink #15100c Colour of the text of the bar on a light section, and of its ground on a dark one; written on the bar as --sticky-header-ink. Options: any opaque CSS colour; an empty string in the settings block writes nothing and leaves the --sticky-header-ink of your stylesheet. data-sticky-header-ground #f4ece0 Colour of the ground of the bar on a light section, and of its text on a dark one; written on the bar as --sticky-header-ground. Give it the colour of your light sections and ink the colour of your dark ones, and the edge of a section runs through the bar with no seam. Options: any opaque CSS colour; an empty string leaves the --sticky-header-ground of your stylesheet. data-sticky-header-accent #ffab1a Colour of the line of the reading progress; written on the bar as --sticky-header-accent. Options: any CSS colour; an empty string leaves the --sticky-header-accent of your stylesheet. data-sticky-header-base light Tone of the page where no section is marked. Options: light, the bar is in ink unless a section says dark; dark, for a dark site, where the light sections are the ones to mark, data-sticky-header-tone="light". data-sticky-header-hide true Whether the bar follows the page away on a scroll down. It always stays near the top and at the foot of the page, while the keyboard is in it, while something in it is open and while one of its links carries the page. Options: true; false, the bar never leaves, and still condenses and takes its tones. data-sticky-header-hide-after 120vh How far the page has to be from its top before the bar may leave: a number of px, or a share of the height of the window written with vh. At the default the bar stays for the whole opening screen and for the first change of section, which is where its cut is first seen. data-sticky-header-tolerance 8 How far the scroll has to go in one direction, in px, before the bar starts to follow it; a smaller movement, a trembling finger or one notch of a precise wheel, leaves it where it is. data-sticky-header-settle 0.6 Time, in seconds, the bar takes to finish a move on its own: when the scroll stops with the bar halfway, when the keyboard calls it back, and for the condensing. data-sticky-header-condense 0.5 Share of its own top and bottom padding the bar gives up once the page has left the top, from 0 to 1. The content never touches: only padding goes. data-sticky-header-condense-after 16 How far the page has to be from its top, in px, before the bar condenses and takes its ground. data-sticky-header-progress true Whether the line under the bar shows the reading progress, and stays at the top of the window while the bar is away. Options: true; false, the hairline alone, and the bar leaves whole. data-sticky-header-roll true Whether each label of the bar, the logo, the links, the button, rolls a line when the tone changes, when the bar comes back and under the pointer of a mouse: the words leave upwards and a copy of them comes in from below, one label after the other from the logo to the button. The engine puts each label in a box of its own for it. Options: true; false, the labels are left as they are written and never move. data-sticky-header-anchor true Whether the engine writes a scroll-padding-top of the height of the condensed bar on the root, so that a link to an id lands below the bar. A page that already has a scroll padding of its own keeps it. Options: true; false.
Two files and their pictures (4), no build step.
It runs in the browser only. Load it with a script tag in plain HTML, in a
client-side script in Astro, never in the frontmatter. In React, call
StickyHeader.mount(ref.current) in useEffect and
destroy() in its cleanup.
-
Tune it, either way
Per element with an attribute, or once for the whole site by editingDEFAULTSat the top of the file.Attribute Default What it is Lower Higher data-sticky-header-ink #15100c Colour of the text of the bar on a light section, and of its ground on a dark one; written on the bar as --sticky-header-ink. Options: any opaque CSS colour; an empty string in the settings block writes nothing and leaves the --sticky-header-ink of your stylesheet. data-sticky-header-ground #f4ece0 Colour of the ground of the bar on a light section, and of its text on a dark one; written on the bar as --sticky-header-ground. Give it the colour of your light sections and ink the colour of your dark ones, and the edge of a section runs through the bar with no seam. Options: any opaque CSS colour; an empty string leaves the --sticky-header-ground of your stylesheet. data-sticky-header-accent #ffab1a Colour of the line of the reading progress; written on the bar as --sticky-header-accent. Options: any CSS colour; an empty string leaves the --sticky-header-accent of your stylesheet. data-sticky-header-base light Tone of the page where no section is marked. Options: light, the bar is in ink unless a section says dark; dark, for a dark site, where the light sections are the ones to mark, data-sticky-header-tone="light". data-sticky-header-hide true Whether the bar follows the page away on a scroll down. It always stays near the top and at the foot of the page, while the keyboard is in it, while something in it is open and while one of its links carries the page. Options: true; false, the bar never leaves, and still condenses and takes its tones. data-sticky-header-hide-after 120vh How far the page has to be from its top before the bar may leave: a number of px, or a share of the height of the window written with vh. At the default the bar stays for the whole opening screen and for the first change of section, which is where its cut is first seen. Under 80vh the bar leaves while the opening screen is still being read; a bare number, 400, is a distance in px Past 200vh the bar covers the top of the page well into the reading data-sticky-header-tolerance 8 How far the scroll has to go in one direction, in px, before the bar starts to follow it; a smaller movement, a trembling finger or one notch of a precise wheel, leaves it where it is. At 0 the bar answers the slightest movement Past 40 the bar seems to wake late, and then jumps to catch up data-sticky-header-settle 0.6 Time, in seconds, the bar takes to finish a move on its own: when the scroll stops with the bar halfway, when the keyboard calls it back, and for the condensing. At 0 it lands at once Past 0.8 the bar is still arriving after the reading has started again data-sticky-header-condense 0.5 Share of its own top and bottom padding the bar gives up once the page has left the top, from 0 to 1. The content never touches: only padding goes. At 0 the bar keeps its height, and only the ground and the hairline come At 1 the content of the bar touches its edges data-sticky-header-condense-after 16 How far the page has to be from its top, in px, before the bar condenses and takes its ground. At 0 the first pixel of scroll condenses it Past 120 the page slides behind a bar that has no ground yet data-sticky-header-progress true Whether the line under the bar shows the reading progress, and stays at the top of the window while the bar is away. Options: true; false, the hairline alone, and the bar leaves whole. data-sticky-header-roll true Whether each label of the bar, the logo, the links, the button, rolls a line when the tone changes, when the bar comes back and under the pointer of a mouse: the words leave upwards and a copy of them comes in from below, one label after the other from the logo to the button. The engine puts each label in a box of its own for it. Options: true; false, the labels are left as they are written and never move. data-sticky-header-anchor true Whether the engine writes a scroll-padding-top of the height of the condensed bar on the root, so that a link to an id lands below the bar. A page that already has a scroll padding of its own keeps it. Options: true; false. -
Drive it from JavaScript
When something else on the page has to hold the bar in place or send it away, a video that plays or a banner that opens, or when the page reacts to the tone of the section on screen.const element = document.querySelector('[data-sticky-header]'); element.addEventListener('stickyheader:tone', (e) => { console.log(e.detail); }); const bar = StickyHeader.mount(element); // the bar auto() mounted, or a new one bar.pin(); // keeps it in place whatever the scroll does; bar.pin(false) lets it go bar.hide(); // bar.show() bar.tone(); // 'light' or 'dark' element.addEventListener('stickyheader:tone', (e) => { console.log(e.detail.tone); }); // When the bar leaves the page: bar.destroy(); // removes what the engine added and gives the markup back as it was
-
Fit it to your page
- Structure:
data-sticky-headeron the bar, aheaderas a rule. That is all the engine needs. What the bar holds is yours: a logo, a row of links, one button, in any markup and with your own classes. - Make it yours: write your bar. Change the words and the links of the logo, of the row and of the button, add or remove links, and give the bar its padding: that padding is its height at the top of the page, and what it gives up as it condenses. The colours are the two of your page,
inkandground, and theaccentof the line. - The tones: mark each dark section of the page
data-sticky-header-tone="dark". A section with no mark is light. While a dark section is behind the bar the two colours change places: the text of the bar takes the colour of the ground, and its ground the colour of the ink. On a dark site setbasetodarkand mark the light sectionsdata-sticky-header-tone="light". A mark can be nested: the innermost one wins. - The button: an element of the bar that is filled with the colour of the text, the button of the preview, carries
data-sticky-header-plate, and its two colours change places with the tone. A link without it only takes the colour of the text. - The labels roll. Every element of the bar that holds words and nothing else is given a window one line tall and a copy of its words, hidden from assistive technology. When the tone changes, when the bar comes back and under the pointer, the words roll out and the copy rolls in, one label after the other.
data-sticky-header-roll="false"on the bar leaves the labels alone. The labels are boxed once, at mount: after rewriting one from a script, destroy and mount again. - What only shows at the top: an element of the bar marked
data-sticky-header-top, the line beside the logo in the preview, fades and lifts away as the bar condenses, and comes back at the top of the page. It keeps its place, so nothing else in the bar moves. - What keeps its colours: a logo in colour, a picture, anything that is not in the two colours of the bar, carries
data-sticky-header-keep. It is left out of the tone and stands over the cut. - The first section starts under the bar. The bar is out of the flow, held to the window, so give the first section of the page a padding at the top that clears it, as the preview does. Nothing jumps when the script arrives: the stylesheet alone holds the bar.
- Anchors: a link to an id of the page lands below the condensed bar, through a
scroll-padding-topthe engine writes on the root. A link of the bar itself keeps the bar in place while it carries the page. - Keyboard: the bar comes back as soon as the keyboard reaches a link in it, and stays while the focus is inside. The focus ring is in the colour of the text.
- With a menu: this is a bar, it opens nothing. For a navigation too long for a row, put the button of the Curtain menu or of the Push menu in the bar,
data-curtain-menu-toggleordata-push-menu-toggle, in place of the row of links or beside it: the bar stays where it is for as long as something in it saysaria-expanded="true", which both buttons do while their menu is open. The Mega menu is a header of its own and the Dock menu a bar at the foot of the window: use one or the other, not both. - On a phone the look stacks the bar in two lines under 767px: the logo and the button, then the row of links spread over the width, each link 44px tall. A row wider than the window scrolls sideways. The bar follows the finger, which is where following the scroll rather than snapping shows most.
- One setting for every bar of the site, without opening the file: declare
window.StickyHeaderSettings = { hideAfter: 400 }in a script placed beforesticky-header.js. The three levels, least specific first, areDEFAULTS, then that object, then adata-sticky-header-*attribute on the bar itself (data-sticky-header-hide-after="400",data-sticky-header-progress="false"). - What the script writes: on the bar,
data-sticky-header-ready,data-sticky-header-state,toporscrolled,data-sticky-header-on,lightordark,data-sticky-header-hiddenwhile it is away, a transform while it is off its place, three custom properties,--sticky-header-k,--sticky-header-riseand--sticky-header-lift, and the three colours. Around each label, a box markeddata-sticky-header-roll. In the bar, six blocks hidden from assistive technology: the ground,data-sticky-header-ground, the hairline,data-sticky-header-line, three plates,data-sticky-header-cut, and the line of the progress,data-sticky-header-fill. On the root,scroll-padding-top. - From JavaScript:
StickyHeader.mount(element)returns the instance, withshow(),hide(),pin()to keep the bar in place andpin(false)to let it go,isHidden(),isCondensed(),tone(),progress(),refresh()after the page changed without a resize, anddestroy(). The bar emitsstickyheader:readyonce mounted,stickyheader:hide,stickyheader:show,stickyheader:toneandstickyheader:condense. - **
prefers-reduced-motion**: the bar does not follow the scroll, it is there or away at once, it condenses at once, and no label rolls. The cut stays: it moves no faster than the page. - Without the script the bar is held at the top of the window by the stylesheet, in ink. A browser whose scripts are off gives it the ground and a hairline, through
@media (scripting: none). - The words of the markup are the preview's, and they ship as they are. Put your own in their place.
- The photographs of the preview are from Unsplash, free to use under the Unsplash licence: the window light by Alexander Possingham (unsplash.com/photos/RIQ96s3Uzso), the plaster wall by Vino Li (unsplash.com/photos/55nuS2rUYmQ), the terracotta wall by Franck Michel (unsplash.com/photos/rwuf4b-1YIA) and the ochre wall by Zeynep S. (unsplash.com/photos/EvlMhRcT1b4). They belong to the page of the preview, not to the bar: swap them for your own.
- On Webflow, add the fonts first. Add Geist and Geist Mono to your site's fonts (Site settings, Fonts, Google Fonts) before pasting: the paste names them, and the Designer drops a font the site does not have.
- On Webflow, the bar is a plain block on the canvas. The Designer runs no Embed, so the bar sits in the flow at the top of the page, where you edit its links like any other. The published page holds it to the window.
- Structure:
-
Avoid the pitfalls
- **Nothing above the bar may carry a
transform, afilteror aperspective.** The bar is fixed to the window; an ancestor with one of those becomes what it is fixed to, and the bar scrolls away with the page. - **Give
inkandgroundthe colours of your sections.** The cut turns one colour exactly into the other. When the ground of the bar is not the colour of the light sections, or its ink not the colour of the dark ones, the edge is still followed but the bar shows as a strip of its own. - Both colours are opaque. A translucent ground lets the page show through the bar, and the plates of the cut then turn the page as well.
- **A picture or a logo in colour needs
data-sticky-header-keep**, or the cut inverts it for the time the edge of a section crosses it. - **The bar must not carry a
transformof its own.** The engine writes one on it while it is off its place. Move what is inside it instead. - Do not give the bar a background. Its ground is the block the engine builds, which is what lets it be absent at the top of the page and cut in two tones. A background on the bar itself hides the cut.
- The page scrolls in the window. A page that scrolls inside a box of its own,
overflow: autoon a wrapper, is not followed. - A page that grows on its own, an accordion that opens, pictures that load late, is measured again by itself. After anything else that moves the sections without changing the size of the page, call
refresh(). - In a framework that owns the DOM, mount in an effect and call
StickyHeader.destroy(el)in its cleanup: the engine boxes the labels, adds six blocks to the bar and takes them back. - Another fixed element at the top of the window, a cookie line, an announcement, is not known to the bar. Put it inside the bar, or give the bar a
topof your own and expect the cut to be off by that much.
- **Nothing above the bar may carry a
One click on Copy the AI prompt copies everything an assistant needs to build this effect: the complete code, the markup, the dependency, every setting and every pitfall.
What it does
Sticky header is an animated navigation component for Webflow and vanilla JavaScript, with no library. Also called sticky navbar hide on scroll, show navbar on scroll up or header change color on scroll section.
The everyday navigation bar of a site, held at the top of the window: a logo, a row of links, one button, in your own markup. At the top of the page it is tall and has no ground. Once the page has left the top it condenses: it gives up part of its padding, a ground comes behind it and a hairline draws itself under it. On a scroll down it does not snap away, it follows the page, one pixel for one pixel, until only its line is left at the top of the window, the thin line of the reading progress; on a scroll up it comes back the same way, and finishes on its own when the scroll stops halfway. It takes the colours of the section behind it, and not with a fade: where a dark section meets a light one behind the bar, the bar is cut on their edge, at the speed of the scroll, the upper part in one tone and the lower part in the other, letters and button split on the same line, so the edge of the page seems to run straight through the bar. A small scroll does not move it, the keyboard always brings it back, it stays while a link of its own carries the page to a section, and anchors land below it. Everything that moves is a transform; the scroll is read once per frame and the layout only when it changes. On a phone the bar is two lines, the logo and the button, then the links. Under reduced motion the bar leaves and comes back with no travel. Without the script it stays at the top on its ground. It is a bar, not a menu: it opens nothing, and pairs with the curtain menu or the push menu, whose button it can hold. Pattern type sticky header, hide on scroll navbar, header that changes colour by section, reading progress bar.
Updated
Questions
Put data-sticky-header on your header: the stylesheet holds it at the top of the window and the engine does the rest. The bar follows the page away, one pixel for one pixel, once the page is data-sticky-header-hide-after from its top, 120vh by default, and comes back the same way on a scroll up. data-sticky-header-hide="false" keeps it in place.
Mark each dark section data-sticky-header-tone="dark". While it is behind the bar, the text takes the colour of ground and the ground of the bar the colour of ink, and where two sections meet the bar is cut on their edge rather than faded, its labels rolling one after the other. A filled button of the bar carries data-sticky-header-plate so its two colours change places too.
The sticky header ignores a movement shorter than data-sticky-header-tolerance, 8 px by default, in one direction. Past it the bar moves by what the page moved, so there is no threshold to flicker around, and when the scroll stops halfway it finishes in data-sticky-header-settle, 0.6 s by default.
Yes. Past data-sticky-header-condense-after, 16 px by default, the sticky header gives up a share of its own padding, data-sticky-header-condense, 0.5 by default, and a ground and a hairline come. It is done with transforms: the box of the bar never changes size and the page is not laid out again.
They land below it: the engine writes a scroll-padding-top of the height of the condensed bar on the root, unless data-sticky-header-anchor="false". The bar comes back as soon as the Tab key reaches a link in it and stays while the focus is inside, and under reduced motion it leaves and returns with no travel.