Programming Intermediate 8 min

How to Build a Sticky Header That Hides on Scroll

A header that sticks to the top while you scroll is table stakes. A header that smartly hides when you scroll down — reclaiming screen real estate — and reappears when you scroll up is a significantly better experience. This guide builds both, progressively.

Step-by-step

  1. 1

    Make the Header Sticky with CSS

    position: sticky keeps the header in the document flow (unlike fixed) and sticks it to the top of the viewport once the user scrolls past it. z-index: 50 ensures it sits above page content.

    css
    .site-header {
      position: sticky;
      top: 0;
      z-index: 50;
      background: #fff;
      /* optional: soften the edge between header and content */
      box-shadow: 0 1px 0 rgb(0 0 0 / .06);
    }
  2. 2

    Define the Hidden State with a CSS Class

    Use a .is-hidden class that slides the header out of view with transform: translateY(-100%). Transform-based animation is GPU-composited — it does not trigger layout reflow and stays smooth at 60 fps even on mid-range devices.

    css
    .site-header {
      position: sticky;
      top: 0;
      z-index: 50;
      background: #fff;
      transition: transform 0.3s ease, box-shadow 0.3s ease;
    }
    
    .site-header.is-hidden {
      transform: translateY(-100%);
    }
  3. 3

    Track Scroll Direction in JavaScript

    Store the previous scroll position in lastScrollY. On each scroll event, compare the current window.scrollY to it. Scrolling down means the new value is larger; scrolling up means it is smaller.

    javascript
    const header = document.querySelector('.site-header');
    let lastScrollY = window.scrollY;
    
    window.addEventListener('scroll', () => {
      const currentScrollY = window.scrollY;
      const scrollingDown = currentScrollY > lastScrollY;
    
      header.classList.toggle('is-hidden', scrollingDown);
    
      lastScrollY = currentScrollY;
    }, { passive: true });
  4. 4

    Add a Scroll Threshold to Prevent Flickering

    Near the very top of the page, small scroll jitter can make the header flicker in and out. Only hide the header after the user has scrolled past a threshold (e.g. the header's own height).

    javascript
    const header = document.querySelector('.site-header');
    const THRESHOLD = header.offsetHeight;
    let lastScrollY = window.scrollY;
    
    window.addEventListener('scroll', () => {
      const currentScrollY = window.scrollY;
    
      // Never hide the header when near the top
      if (currentScrollY < THRESHOLD) {
        header.classList.remove('is-hidden');
        lastScrollY = currentScrollY;
        return;
      }
    
      const scrollingDown = currentScrollY > lastScrollY;
      header.classList.toggle('is-hidden', scrollingDown);
    
      lastScrollY = currentScrollY;
    }, { passive: true });
  5. 5

    Shrink the Header on Scroll with CSS

    A complementary pattern: shrink the header's padding (and optionally its logo size) when the user scrolls down, to reclaim a few pixels while keeping the header visible. Toggle a .is-scrolled class instead of hiding it entirely.

    css
    .site-header {
      position: sticky;
      top: 0;
      z-index: 50;
      padding-block: 1.25rem;
      transition: padding 0.3s ease, box-shadow 0.3s ease;
    }
    
    .site-header.is-scrolled {
      padding-block: 0.5rem;
      box-shadow: 0 2px 12px rgb(0 0 0 / .1);
    }
  6. 6

    Experiment with Scroll-Driven Animations

    Modern browsers support scroll-driven animations — a pure-CSS way to animate based on scroll position, with zero JavaScript. The example below shrinks the header's background-color as the user scrolls, using animation-timeline: scroll(). Browser support is Chromium 115+ and Firefox 110+ — use it as progressive enhancement.

    css
    @supports (animation-timeline: scroll()) {
      .site-header {
        animation: shrink-header linear both;
        animation-timeline: scroll();
        animation-range: 0px 80px;
      }
    
      @keyframes shrink-header {
        from { padding-block: 1.25rem; background: transparent; }
        to   { padding-block: 0.5rem;  background: #fff; box-shadow: 0 1px 8px rgb(0 0 0 / .08); }
      }
    }
  7. 7

    Handle Accessibility — Keep Focus Inside

    When the header hides, interactive elements inside it (navigation links, a search button) become invisible but remain in the tab order. Add inert to the header when it is hidden, and remove it when it reappears. This removes the element from both tab focus and screen reader announcement without removing it from the DOM.

    javascript
    window.addEventListener('scroll', () => {
      const currentScrollY = window.scrollY;
      if (currentScrollY < THRESHOLD) {
        header.classList.remove('is-hidden');
        header.removeAttribute('inert');
        lastScrollY = currentScrollY;
        return;
      }
    
      const scrollingDown = currentScrollY > lastScrollY;
      header.classList.toggle('is-hidden', scrollingDown);
      header.toggleAttribute('inert', scrollingDown);
    
      lastScrollY = currentScrollY;
    }, { passive: true });

Tips & gotchas

  • Always mark your scroll listener as <code>{ passive: true }</code> — this tells the browser the handler will never call <code>preventDefault()</code>, enabling scroll-performance optimisations.
  • If your page has anchor links, a hidden sticky header will overlap the target. Compensate with <code>scroll-margin-top</code> on anchored elements: <code>:target { scroll-margin-top: 80px; }</code>.
  • For menus opened inside the header (dropdowns, mobile nav), pause the hide-on-scroll logic while the menu is open — otherwise the header may vanish mid-interaction.
  • <code>position: sticky</code> requires the parent not to have <code>overflow: hidden</code> or <code>overflow: auto</code> set — that clips the stickiness.

Wrapping up

A well-built sticky header is a two-part contract: CSS owns the animation (smooth, GPU-composited), and JavaScript owns only the state decision (hide or show). Keep the scroll handler lightweight and passive, add the threshold to eliminate flicker near the top, and handle the inert attribute to keep the experience accessible. The scroll-driven animation API will eventually replace the JS approach entirely — start with the CSS-first pattern now.

#CSS #JavaScript #UX
Back to all guides

Need Help With Your Project?

Book a free 30-minute consultation to discuss your technical challenges and explore solutions together.