Sticky Header: CSS position: sticky and Hide-on-Scroll

Give the header position: sticky and top: 0, plus a background and a z-index, and it stays pinned while the page scrolls. It only sticks inside its parent, and an ancestor with overflow set can stop it. For a header that hides on scroll, toggle a translateY class from a scroll listener.

sticky vs fixed for headers
position: stickyposition: fixed
Takes up space in the layoutYesNo, add padding to the body
Pinned relative toNearest scrolling ancestorThe viewport
Needs an inset such as top: 0Yes, or it acts like relativeUsually
Creates a stacking contextYesYes
Stops at the end of its parentYesNo

Updated

Sticky headers are one line of CSS until they are not: the header refuses to stick, anchor links land under it, or a dropdown slides behind the content. This page covers the line of CSS, then each of those failures, with a live scroll demo you can switch between plain, shrinking and hiding modes.

Live scroll demo

Switch between plain, shrink and hide-on-scroll, then scroll inside the box.

The basic sticky header

Sticky header inside a scrolling page

Scroll the frame. The header stays at the top and the anchor link stops below it thanks to scroll-padding-top.

<style>
  html { scroll-padding-top: 56px }
  body { margin: 0 }
  .site-header { position: sticky; top: 0; z-index: 50; height: 48px; display: flex; align-items: center; gap: 16px;
                 padding: 0 16px; background: rgb(255 255 255 / .92); border-bottom: 1px solid #e2e8f0 }
  .site-header a { color: #1d4ed8 }
  section { min-height: 260px; padding: 16px }
  section:nth-child(odd) { background: #f8fafc }
</style>
<header class="site-header"><strong>Brand</strong><a href="#pricing">Pricing</a></header>
<main>
  <section><p>Scroll down.</p></section>
  <section id="pricing"><h2>Pricing</h2></section>
  <section></section>
</main>

Hide on scroll down, show on scroll up

CSS for the hidden and shrunk states

The script only sets data attributes. Every visual change lives in CSS.

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  transition: transform 200ms ease;
}
.site-header[data-hidden="true"] {
  transform: translateY(-100%);
}
.site-header[data-scrolled="true"] {
  box-shadow: 0 1px 8px rgb(0 0 0 / 0.08);
}
@media (prefers-reduced-motion: reduce) {
  .site-header { transition: none; }
}

React hook: useHideOnScroll

Returns two booleans you can spread onto the header as data attributes. The 8px threshold ignores tiny jitters.

import { useEffect, useState } from "react";

export function useHideOnScroll(threshold = 8) {
  const [state, setState] = useState({ hidden: false, scrolled: false });
  useEffect(() => {
    let last = window.scrollY;
    const onScroll = () => {
      const y = window.scrollY;
      const delta = y - last;
      if (Math.abs(delta) < threshold) return;
      setState({ hidden: delta > 0 && y > 64, scrolled: y > 0 });
      last = y;
    };
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => window.removeEventListener("scroll", onScroll);
  }, [threshold]);
  return state;
}

// <header className="site-header" data-hidden={hidden} data-scrolled={scrolled}>

Sticky header pitfalls

Symptoms and fixes
SymptomCauseFix
Header scrolls awayNo top valueAdd top: 0
Header scrolls awayAncestor has overflow: hiddenMove overflow to a smaller wrapper, or use overflow: clip
Sticks, then leaves earlyParent is shortPlace the header in body or a full-height wrapper
Anchor headings hiddenNo scroll offsetscroll-padding-top on html
Menu goes behind contentHeader stacking context too lowz-index on the header itself
Content shows throughTransparent backgroundGive the header a background colour

How sticky really works

A sticky element starts in normal flow. Once scrolling would carry it past its inset, top: 0 for a header, it holds that position until the bottom of its parent arrives and pushes it away.

The box it sticks to is the nearest ancestor with a scrolling mechanism, which includes overflow: hidden. That is why a wrapper added to clip a carousel can silently break a header far away from it.

Hide on scroll down, show on scroll up

Compare the current scroll position with the last one. Moving down past a small threshold hides the header with transform: translateY(-100%); any upward movement brings it back.

Animate transform rather than top or height, since a transform does not trigger layout. Listen to scroll with the passive option so the browser never waits for your handler.

Shrinking without layout jumps

Changing the header height while sticky moves the content under it. Keep the outer height fixed and shrink the logo with transform: scale, or reduce padding inside a header that is already out of the way.

A data attribute such as data-scrolled on the header keeps the style rules in CSS and the script down to one line.

How it works

  1. Scroll inside the demo box. The header is sticky to that box, the same way a real header sticks to the page.
  2. Switch the mode to shrink or hide-on-scroll and scroll again in both directions.
  3. Click a section link in the demo to see scroll-margin-top keep the heading clear of the header.
  4. Copy the CSS and the hook that match the mode you want.

Tips

Frequently asked questions

Why is my position: sticky not working?

Three usual causes: no inset value such as top: 0, a parent that ends right after the header so there is no room to stick, or an ancestor with overflow other than visible, which becomes the box the header sticks to. Check them in that order.

Should a header be sticky or fixed?

Sticky is simpler because it keeps its place in the layout, so nothing jumps under it at the top of the page. Fixed is useful when the header must sit over a full-screen hero, but then you add padding to the page yourself.

How do I stop anchor links hiding under a sticky header?

Add scroll-padding-top to the scrolling container, usually html, equal to the header height. For one-off targets you can instead put scroll-margin-top on the heading itself.

Why does my dropdown appear behind the page content?

Sticky positioning creates a stacking context, so anything inside the header can only stack within it. Raise the header itself with a z-index above later content, for example z-index: 50, rather than raising the dropdown.

Related: CSS position explained, z-index and stacking contexts, Scroll animations, Website header examples, Respecting prefers-reduced-motion.

Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.

More in Guides

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap