CSS Auto-Hiding Header with scroll-state(scrolled)
Auto-hiding header without scroll event listeners
Hiding the header on scroll-down and revealing it on scroll-up used to mean tracking scroll position in JavaScript and comparing it to the previous value. CSS scroll-state() with the scrolled descriptor reports the direction of the most recent scroll natively.
let lastY = window.scrollY;window.addEventListener( 'scroll', () => { const y = window .scrollY; const dir = y > lastY ? 'down' : 'up'; document.querySelector( 'header') .classList.toggle( 'hidden', dir === 'down'); lastY = y;}); html { container-type: scroll-state;}header { position: fixed; inset-block-start: 0; transition: translate .3s;}@container scroll-state( /* [!code ++] */scrolled: block-end /* [!code ++] */) { header { translate: 0 -100%; }} container scroll state queries Browser Support
This feature is not Baseline because it does not work in some of the most widely-used browsers.
Not ready for production without a fallback.
No scroll listeners
The browser reports the most recent scroll direction internally. No scroll event handlers, no stored lastY, no manual comparison logic.
Direction values
Query scrolled: top, bottom, left, right, or the logical block-start, block-end, inline-start, inline-end. Use scrolled: none for the initial state.
Animates with transitions
State changes flow through normal CSS transitions. Pair scroll direction with translate or opacity for a smooth slide or fade.
How it works
Auto-hiding headers (the kind that slide away when scrolling down and slide back when scrolling up) have always required JavaScript. You attach a scroll listener, remember the previous scroll position, compare it to the current one, and toggle a class based on the direction. It works, but it runs on every scroll event, fights the main thread, and adds state to track.
scroll-state() container queries with the scrolled descriptor expose the most recent scroll direction directly to CSS. Set container-type: scroll-state on the scrolling ancestor, then write @container scroll-state(scrolled: block-end) to match when the user just scrolled toward the end of the document. Add a CSS transition on translate and the header slides itself away. No listeners, no script, no class toggling.