GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

9 CSS Sticky Header Effects — No JS, Scroll-Driven

A css sticky header is a header pinned with position: sticky so it locks to the top of the viewport once the page scrolls past it.

Auto-plays · click a tile to jump to its section · all nine in one zip

The order below moves from a header noticing scroll entirely on its own — hiding, shading, inverting — to a header holding still while one internal piece reacts. Finally, it moves to a second element altogether (a bar, a sub-nav row, a label, a set of dots) synced to whatever the header is doing. Each tile in the grid above loops on its own two-second timer inside an iframe with no scrollbar at all, so there is nothing to drag there — just watch it play through once. When scroll should move the page's layers instead of its header, the CSS parallax scrolling set is the same timeline put to different work, and a header whose contents keep drifting past rather than stacking is the CSS marquee infinite loop.

01Hide then reveal

The motion

The header slides fully off the top of the screen the moment you scroll down, and the instant the scroll direction flips upward it slides straight back, even from far down the page.

Hide then reveal
Open in a new tab

01 · Hide then reveal — Measured area 3.501% · moving frames 9/23Original by GODRICH · SCSS and React ship in the zip · See the password

position:stickytranslateY(-100%)방향 감지

Where a css sticky header on scroll fits

That direction check is the entire trick — no distance threshold decides when the header disappears, only which way the scroll just moved. It suits long article pages and mobile shop headers, where a header that stays visible the whole time eats space, while one that only reappears on demand does not.

var head = document.querySelector('.head');
var lastY = window.scrollY;
window.addEventListener('scroll', function () {
  var y = window.scrollY;
  head.classList.toggle('is-hidden', y > lastY && y > 40);
  lastY = y;
}, { passive: true });

02Shadow appears on scroll

What it does

At the very top of the page the header sits flush with no border at all; the first pixel of scroll then switches on a soft shadow layer that was already drawn into the markup, invisible until then.

Shadow appears on scroll
Open in a new tab

02 · Shadow appears on scroll — Measured area 15.0% · moving frames 13/23Original by GODRICH · SCSS and React ship in the zip · See the password

IntersectionObserver그림자 레이어 opacity0→1

When to use it

Because the shadow is a separate opacity-only layer rather than a live box-shadow value, toggling it costs almost nothing. Reach for it on documentation and blog headers over a pale background, where a shadow only needs to exist once there is something below it to separate from.

.head__shadow {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -48px;
  height: 48px;
  background: linear-gradient(rgba($stage-ink, .85), rgba($stage-ink, 0));
  opacity: 0;
  transition: opacity $dur-quick $ease-out;
}

.head.is-scrolled .head__shadow { opacity: 1; }

03Color invert on scroll

What you see

Over a dark hero the header shows white text on a transparent background, and the moment the hero scrolls out of view a second, fully opaque layer crossfades in underneath it, flipping the header's whole palette at once.

Color invert on scroll
Open in a new tab

03 · Color invert on scroll — Measured area 18.667% · moving frames 13/23Original by GODRICH · SCSS and React ship in the zip · See the password

IntersectionObserver색 토큰 교체히어로 높이 기준

Where it fits

Nothing about the logo or nav markup changes — only which of the two stacked background layers happens to be opaque. It's built for landing pages with a full-bleed hero image, where one header needs to read against two completely different backgrounds without ever going invisible.

.head__layer--dark {
  background: transparent;
  opacity: 1;
  transition: opacity $dur-base $ease-out;
}
.head__layer--light {
  background: $subject-ink;
  opacity: 0;
  transition: opacity $dur-base $ease-out;
}
.head.is-inverted .head__layer--dark  { opacity: 0; }
.head.is-inverted .head__layer--light { opacity: 1; }

04Logo-only shrink

The motion

Only the logo mark scales down as the page scrolls — the header's height, background, and every other child sit exactly where they started, so nothing around the logo ever has to reflow.

Logo-only shrink
Open in a new tab

04 · Logo-only shrink — Measured area 1.585% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

transform:scale높이 고정transform-origin:left

Good places for it

Where it's supported, the scale is driven straight off document scroll progress through animation-timeline: scroll(), with no scroll listener written at all. It's the shape to reach for on a SaaS landing page with a large brand mark, anywhere a shrinking header would otherwise cause layout shift.

.head {
  height: 56px; // fixed height and background never move

  @supports (animation-timeline: scroll()) {
    .head__logo {
      animation: logo-shrink linear both;
      animation-timeline: scroll(root);
      animation-range: 0 150px;
    }
  }
}

@keyframes logo-shrink {
  from { transform: scale(1); }
  to   { transform: scale(.72); }
}

05Progress bar attached to header

What it does

A three-pixel bar fused to the header's own bottom edge fills left to right as reading progresses, moving as one piece with the header because it is a child of the header rather than a separately positioned strip.

Progress bar attached to header
Open in a new tab

05 · Progress bar attached to header — Measured area 1.0% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

scroll() 타임라인하단 3px헤더 자식 요소

When to use it

Like the logo above, its fill is tied to animation-timeline: scroll(root) instead of a scroll handler recalculating a percentage on every frame. Long blog posts and tutorial docs are the natural home for it, giving a reader a sense of how much is left without a separate progress widget on the page.

.head__bar {
  position: absolute;
  left: 0;
  bottom: -3px;
  width: 100%;
  height: 3px;
  background: $subject-blue;
  transform: scaleX(0);
  transform-origin: left;

  @supports (animation-timeline: scroll()) {
    animation: fill-bar linear both;
    animation-timeline: scroll(root);
  }
}

@keyframes fill-bar {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

06Sticky sub-nav below header

What you see

A row of category tabs sits directly beneath the main header, and once it reaches the header's own height it locks into place as a second sticky row, traveling with the header instead of scrolling under it.

Sticky sub-nav below header
Open in a new tab

06 · Sticky sub-nav below header — Measured area 1.333% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

top:헤더높이이중 sticky겹침 없음

Where it fits

The two never fight over the same offset — the sub-nav's sticky top is set to the header's fixed height, not to zero. It's built for e-commerce category tabs and docs-site section tabs, anywhere a second navigation row needs to stay reachable the whole time the main header is visible.

.head { position: sticky; top: 0; z-index: 2; height: 56px; }

.subnav {
  position: sticky;
  top: 56px;
  z-index: 1;
  border-bottom: 4px solid transparent;
  transition: border-color $dur-quick $ease-out;

  &.is-stuck { border-bottom-color: $subject-cream; }
}

07Sticky current section title

The motion

Instead of a fixed header title, the header slot shows only the name of whichever section of a long list is currently passing underneath it, swapping labels the moment the next section starts.

Sticky current section title
Open in a new tab

07 · Sticky current section title — Measured area 0.241% · moving frames 12/23Original by GODRICH · SCSS and React ship in the zip · See the password

IntersectionObserver센티넬 요소제목 교체

Good places for it

A thin sentinel element sits at the top of every section, and an observer watching each one decides which label counts as current. Alphabetized contact lists, glossaries, and long FAQ pages are where it earns its keep, replacing a static header with one that always names where the reader is.

var labels = document.querySelectorAll('.head__label');
document.querySelectorAll('.list__sentinel').forEach(function (s) {
  new IntersectionObserver(function (es) {
    if (es[0].isIntersecting) {
      labels.forEach(function (l) {
        l.classList.toggle('is-current', l.textContent === s.dataset.label);
      });
    }
  }, { rootMargin: '-56px 0px -80% 0px' }).observe(s);
});

08Backdrop-blur glass header

What it does

Scrolling past the top does not turn this header opaque — it switches on a backdrop-filter blur instead, so whatever sits behind it keeps showing through, just softened into an iOS-style frosted pane.

Backdrop-blur glass header
Open in a new tab

08 · Backdrop-blur glass header — Measured area 18.283% · moving frames 11/23Original by GODRICH · SCSS and React ship in the zip · See the password

backdrop-filter:blur반투명 배경IntersectionObserver

When to use it

It belongs on headers layered over a hero image or video, anywhere painting the background flat white would hide too much of what's underneath it.

.head {
  background: rgba(255, 255, 255, .08);
  backdrop-filter: blur(0);
  transition: background $dur-base $ease-out, backdrop-filter $dur-base $ease-out;

  &.is-scrolled {
    background: rgba(255, 255, 255, .55);
    backdrop-filter: blur(10px);
  }
}

09Header synced to scroll-snap

What you see

When the page snaps from section to section through scroll-snap-type, a row of dots inside the header advances to match whichever section just settled, growing the current dot and shrinking the one it replaces.

Header synced to scroll-snap
Open in a new tab

09 · Header synced to scroll-snap — Measured area 0.281% · moving frames 9/23Original by GODRICH · SCSS and React ship in the zip · See the password

scroll-snap-typeIntersectionObserver인디케이터 동기화

Where it fits

Nothing about the snapping itself lives in the header — an observer watching each section is what flips which dot counts as current. It suits full-page storytelling sites and slide-style product tours, where the header's only job is telling the visitor which slide they landed on.

.stage { scroll-snap-type: y mandatory; }

.head__dots i {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: rgba(255, 255, 255, .3);
  transform: scale(1);
  transition: transform $dur-quick $ease-pop, background $dur-quick $ease-pop;

  &.is-current { background: $subject-cream; transform: scale(1.4); }
}

Where it breaks — the traps

Two silent breakages

Two different breakages turn up across these nine, and neither one throws an error to tell you what happened. The sticky-dependent group — 01, 02, 03, 06, 08, and 09 — all rely on position: sticky finding room to move. Give any ancestor between the header and the actual scrolling element overflow: hidden or overflow: auto, or leave that ancestor without an explicit height, and the header just stops sticking, scrolling off with everything else as though the sticky rule was never written. Position: sticky itself has shipped in every modern browser for years now — when it stops working, the bug almost always sits in a parent's overflow or height, not in the sticky element itself. The second trap lives in items 04 and 05, both of which drive their transform off animation-timeline: scroll(root). That syntax only reached Chrome at version 115 and Safari at 26, with Firefox still shipping it behind a flag as of September 2026. Without the @supports (animation-timeline: scroll()) guard already sitting inside both demos above, a Firefox visitor just gets a logo that never shrinks and a bar that never fills, not a crash. Pull all nine together with every fallback already wired in from the archive, unzipped with u9rbcprm typed exactly as it reads on this line, no extra spaces added anywhere. Under a pinned header like this one, a body headline that decodes open instead of simply appearing is the trick the text reveal decode effects hold.

Accessibility

Reduced motion

None of the nine keep animating once prefers-reduced-motion is set, but each one settles on a different resting frame rather than freezing mid-loop. Header 01 stays fully visible with no slide-away at all; 02's shadow and 03's inverted colors both lock straight into their scrolled-past state instead of fading in gradually. 04's logo and 09's current dot hold their post-scroll size directly, skipping the scale animation between the two states entirely. 05's progress bar and 06's sub-nav border both settle full and lit rather than sweeping in over time, and 08's blur simply stays on rather than toggling with scroll position. 07 just stops sliding one label out from under the other, leaving whichever was already current sitting still. MDN's reference for prefers-reduced-motion covers how each operating system exposes the setting to the browser.

@media (prefers-reduced-motion: reduce) {
  .head, .head__shadow, .head__layer--light, .head__logo,
  .head__bar, .subnav, .head__label, .head__dots i {
    transition: none;
    animation: none !important;
  }
}

More css sticky header example patterns and the rest of the css collection live at the css category hub, and what this site actually sells — zips, not tutorials — is explained on the about page.

FAQ

Why is my css sticky header not working partway down the page?

The most common cause is a parent element with overflow: hidden, overflow: auto, or overflow: scroll sitting between the sticky header and the page's real scroll container — position: sticky only sticks inside its nearest scrolling ancestor, and clipped overflow on that ancestor silently breaks it. The second most common cause is a parent that never gets an explicit height, leaving sticky no room to move inside. Check both of those before assuming the property itself is unsupported. Adding a search bar, CTA, or badge onto the pinned bar is the step the sticky header extras pick up from here.

Can I change a css sticky header's background on scroll without JavaScript?

Partly. Items 02 and 08 both use a pre-drawn layer whose opacity gets toggled, but the toggle itself still needs a few lines of JavaScript — an IntersectionObserver watching a sentinel element — because CSS alone has no built-in way to ask whether the user has scrolled past a given point. Items 04 and 05 get closer to JS-free by tying a transform straight to animation-timeline: scroll(), but that only handles continuous scroll-linked change, not an on/off swap at one fixed point.

Does a css sticky header table work inside a scrollable wrapper?

Yes, but the sticky context shifts: a header cell needs position: sticky with top: 0 set relative to the table's own scrolling wrapper, not to the page. None of the nine patterns above target that case directly, since all nine assume the header sticks to the page itself, but the same is-scrolled toggle from items 02 and 08 carries over cleanly once the sticky context becomes the table wrapper instead of the window.

Enter the archive password

The password is inside this article. You will find it as you read.