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.

Old way 10 lines
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;});
Modern
6 lines
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%;  }}
Limited availability 50% global usage

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.

144+
144+
scroll down to hide the header, scroll up to show it
Auto-Hiding Header visible
Content row 1
Content row 2
Content row 3
Content row 4
Content row 5
Content row 6
Content row 7
Content row 8
Content row 9
Content row 10
Content row 11
Content row 12
Content row 13
Content row 14
@container scroll-state(scrolled: block-end)

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.

Lines Saved
10 → 6
No JS required
Old Approach
scroll listener
Manual direction track
Modern Approach
scroll-state()
scrolled descriptor

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.

Accessibility note: Hiding the header when scrolling down still leaves it reachable: scrolling up brings it back, and the underlying element stays in the accessibility tree. Avoid hiding navigation that is critical to the page (such as a skip link or the only path to primary actions) without an alternative entry point.
ESC