GODRICH

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.

01Hide then reveal

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. 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.

position:stickytranslateY(-100%)방향 감지
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

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. 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.

IntersectionObserver그림자 레이어 opacity0→1
.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

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. 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.

scroll() 타임라인색 토큰 교체히어로 높이 기준
.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

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. 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.

transform:scale높이 고정transform-origin:left
.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

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. 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.

scroll() 타임라인하단 3px헤더 자식 요소
.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

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. 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.

top:헤더높이이중 sticky겹침 없음
.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

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. 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.

IntersectionObserver센티넬 요소제목 교체
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

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. 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.

backdrop-filter:blur반투명 배경IntersectionObserver
.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

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. 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.

scroll-snap-typeIntersectionObserver인디케이터 동기화
.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 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.

Accessibility

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;
  }
}

The rest of the css collection lives at the css category hub, and what this site actually sells — zips, not tutorials — is explained on the about page.

FAQ

Why does my css sticky header stop 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.

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 work inside a scrollable table?

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.