25 CSS Sticky Navigations
07 / 25

All 25 in 25 CSS Sticky Navigations

  1. 01Pure CSS Sticky Navbar (No JavaScript, position:sticky)
  2. 02Sticky Top Header with Flexbox Layout (Logo · Links · CTA)
  3. 03Responsive Sticky Navbar with Hamburger Menu Toggle
  4. 04Sticky Header with Full-Bleed Bar and Container Max-Width Content
  5. 05Floating Glass Pill Sticky Navbar (Rounded, Backdrop Blur, Elevation)
  6. 06Sticky Header Shrink on Scroll (80px → 50px, Compact State)
  7. 07Sticky Nav with Directional Hide (Scroll Down) and Reveal (Scroll Up)
  8. 08Transparent-to-Solid Sticky Header on Hero Scroll
  9. 09Sticky Header with Article Reading Progress Bar Underneath
  10. 10Sticky Header with SVG Logo Wordmark → Mark-Only Morph on Scroll
  11. 11Sticky Navbar with Underline-Slide Active Link Indicator
  12. 12Sticky Header with Theme-Aware Token Swap (Light/Dark)
  13. 13Minimalist Sticky Header with Single High-Emphasis CTA
  14. 14Split Sticky Navbar with Centered Brand Logo (Links Left + Right)
  15. 15Neumorphic Sticky Navigation Bar (Soft-UI Inset/Outset Shadows)
  16. 16Sticky Header with Multi-Column Mega Menu Dropdown
  17. 17E-Commerce Sticky Header with Search Bar and Cart Badge Counter
  18. 18Sticky Announcement Bar Stacked Above Sticky Nav (Dual-Stick)
  19. 19Sticky Sub-Navigation Bar Locked Under the Main Sticky Header
  20. 20Sticky Header with Icon-to-Full-Width Search Expand on Click
  21. 21Sticky Sidebar Table of Contents That Follows a Long Article
  22. 22Mobile Bottom Tab Bar with Safe-Area Inset Padding
  23. 23Active-Section Scrollspy Highlighting in a Sticky Navbar
  24. 24Sticky Horizontal Scroll Tab Bar with Snap Points
  25. 25Sticky Header with Social Share Icons for Articles
Light JSMIT licensed

Sticky Nav with Directional Hide (Scroll Down) and Reveal (Scroll Up)

Reading-first navigation: the header slides out of the way while the reader moves down the page and snaps back the instant they scroll up, so the nav is always one small gesture away without permanently costing 64px of screen. The demo focuses on the two details that separate a polished implementation from a twitchy one — a movement dead zone and a rest state at the very top.

Live Demo
Plain CSSTailwind v4
Try it

Complete Tailwind CSS landing page templates in HTML, React, Next.js and Vue · from $49

Cruip · Affiliate link

Browse Cruip templates

The code

<section class="sn-07" aria-label="Directional hide and reveal sticky nav demo">
    <header class="sn-07__bar" id="sn-07-bar" data-hidden="false">
      <a class="sn-07__brand" href="#sn-07-s1"><span class="sn-07__logo" aria-hidden="true"></span>Dispatch</a>
      <nav class="sn-07__nav" aria-label="Primary">
        <a class="sn-07__link" href="#sn-07-s1" aria-current="page">World</a>
        <a class="sn-07__link" href="#sn-07-s2">Business</a>
        <a class="sn-07__link" href="#sn-07-s3">Culture</a>
      </nav>
      <span class="sn-07__state" id="sn-07-state" aria-live="off">visible</span>
    </header>
  <div class="sn-07__intro">
    <div class="sn-07__introin">
    <p class="sn-07__intro-eyebrow">scroll delta · dead zone · rAF</p>
    <h2 class="sn-07__intro-title">Down it goes, up it comes back</h2>
    <p class="sn-07__intro-sub">Scroll down past the intro, then reverse by a few pixels.</p>
    </div>
  </div>
    <main>
      <section class="sn-07__sec" id="sn-07-s1">
        <p class="sn-07__kicker">World</p>
        <h3 class="sn-07__h">Screen space back to the reader</h3>
        <p class="sn-07__p">A permanently pinned bar costs 64px on every screen of a long article. Directional hiding gives that space back and keeps the nav one gesture away.</p>
      </section>
      <section class="sn-07__sec" id="sn-07-s2">
        <p class="sn-07__kicker">Business</p>
        <h3 class="sn-07__h">The dead zone matters most</h3>
        <p class="sn-07__p">Without a movement threshold the bar flickers on trackpad jitter and elastic bounce. Six pixels is usually the sweet spot.</p>
      </section>
      <section class="sn-07__sec" id="sn-07-s3">
        <p class="sn-07__kicker">Culture</p>
        <h3 class="sn-07__h">Never hide near the top</h3>
        <p class="sn-07__p">Inside the first 120px, hiding reads as a bug. The rest zone keeps the header planted where users expect to find it.</p>
      </section>
      <section class="sn-07__sec">
        <p class="sn-07__kicker">Accessibility</p>
        <h3 class="sn-07__h">Focus pulls it back</h3>
        <p class="sn-07__p">A <code>:focus-within</code> rule overrides the hidden state, so tabbing into the nav can never leave a keyboard user chasing an off-screen link.</p>
      </section>
    </main>
  <footer class="sn-07__pagefoot">
    <p class="sn-07__pagefootin">Sticky navigation pattern 07 of 25 · codefronts.com</p>
  </footer>
</section>
.sn-07 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: var(--sn-07-bg);
  --sn-07-bg: oklch(0.17 0.02 275);
  --sn-07-surface: oklch(0.215 0.024 275);
  --sn-07-ink: oklch(0.97 0.006 275);
  --sn-07-mut: oklch(0.72 0.018 275);
  --sn-07-acc: oklch(0.82 0.17 85);
  --sn-07-line: oklch(1 0 0/.1);
  --sn-07-h: 62px;
  font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
  color: var(--sn-07-ink);
  -webkit-font-smoothing: antialiased;
}

.sn-07 *,
.sn-07 *::before,
.sn-07 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.sn-07__eyebrow,
.sn-07__intro-eyebrow {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11.5px;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--sn-07-acc);
}

.sn-07__title,
.sn-07__intro-title {
  font-size: clamp(23px,3.5vw,37px);
  line-height: 1.06;
  letter-spacing: -.03em;
  font-weight: 700;
  text-wrap: balance;
}

.sn-07__sub,
.sn-07__intro-sub {
  font-size: 14.4px;
  line-height: 1.6;
  color: var(--sn-07-mut);
  text-wrap: pretty;
}

.sn-07__bar {
  position: sticky;
  top: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  gap: clamp(10px,2vw,22px);
  min-height: var(--sn-07-h);
  padding: 0 clamp(14px,2.4vw,22px);
  background: color-mix(in oklch,var(--sn-07-surface) 84%,transparent);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border-bottom: 1px solid var(--sn-07-line);
  will-change: translate;
  transition: translate .34s cubic-bezier(.32,.72,.3,1);
}

.sn-07__bar[data-hidden="true"] {
  translate: 0 -100%;
}

.sn-07__bar:focus-within {
  translate: 0 0;
}

.sn-07__brand {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -.025em;
  color: inherit;
  text-decoration: none;
}

.sn-07__logo {
  width: 20px;
  height: 20px;
  border-radius: 5px;
  background: var(--sn-07-acc);
  clip-path: polygon(0 0,100% 0,100% 62%,50% 100%,0 62%);
}

.sn-07__nav {
  display: flex;
  align-items: center;
  gap: 2px;
  margin-inline-start: auto;
}

.sn-07__link {
  display: flex;
  align-items: center;
  min-height: 36px;
  padding: 0 12px;
  border-radius: 9px;
  font-size: 13.6px;
  font-weight: 600;
  color: var(--sn-07-mut);
  text-decoration: none;
  transition: color .18s ease,background .18s ease;
}

.sn-07__link:hover {
  color: var(--sn-07-ink);
  background: oklch(1 0 0/.07);
}

.sn-07__link[aria-current="page"] {
  color: var(--sn-07-acc);
}

.sn-07__link:focus-visible,
.sn-07__brand:focus-visible {
  outline: 2px solid var(--sn-07-acc);
  outline-offset: 2px;
}

.sn-07__state {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 10.5px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--sn-07-acc);
  padding: 5px 11px;
  border-radius: 99px;
  border: 1px solid color-mix(in oklch,var(--sn-07-acc) 32%,transparent);
  background: color-mix(in oklch,var(--sn-07-acc) 10%,transparent);
  white-space: nowrap;
}

.sn-07__sec {
  scroll-margin-top: var(--sn-07-h);
  display: grid;
  gap: 10px;
  align-content: center;
  min-height: 66svh;
  padding: clamp(24px,4vw,44px);
  border-bottom: 1px solid var(--sn-07-line);
}

.sn-07__sec:nth-child(even) {
  background: oklch(1 0 0/.025);
}

.sn-07__kicker {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--sn-07-acc);
}

.sn-07__h {
  font-size: clamp(19px,2.6vw,26px);
  line-height: 1.16;
  letter-spacing: -.025em;
  font-weight: 690;
  text-wrap: balance;
}

.sn-07__p {
  max-width: 52ch;
  font-size: 14.8px;
  line-height: 1.64;
  color: var(--sn-07-mut);
  text-wrap: pretty;
}

.sn-07__p code {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: .85em;
  background: oklch(1 0 0/.09);
  padding: 2px 6px;
  border-radius: 5px;
}

@media (max-width: 560px) {
  .sn-07__state {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .sn-07__bar[data-hidden="true"] {
    translate: 0 0;
  }

  .sn-07 * {
    transition-duration: .01ms !important;
  }
}
/* ── full-page mode: the demo IS the document, so sticky pins against the real viewport ── */

:root:has(.sn-07) {
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--sn-07-h) + 10px);
}

@media (prefers-reduced-motion: reduce) {
  :root:has(.sn-07) {
    scroll-behavior: auto;
  }
}

.sn-07__bar,
.sn-07__barin {
  padding-inline: max(clamp(16px,3vw,26px),calc((100% - 1280px)/2));
}

.sn-07__sec {
  padding-inline: max(clamp(22px,5vw,44px),calc((100% - 1120px)/2));
}

.sn-07__intro {
  display: grid;
  place-items: center;
  min-height: min(90svh,900px);
  padding: clamp(56px,12vh,150px) clamp(20px,5vw,48px) clamp(40px,8vh,90px);
  background: radial-gradient(85% 55% at 50% 0%,oklch(0.3 0.06 85/.4),transparent);
}

.sn-07__introin {
  display: grid;
  justify-items: center;
  gap: clamp(12px,1.8vw,18px);
  text-align: center;
  max-width: 26ch;
}

.sn-07__intro-eyebrow {
  font-size: clamp(11px,1.1vw,13px);
}

.sn-07__intro-title {
  font-size: clamp(38px,7.5vw,86px);
  line-height: .98;
  letter-spacing: -.04em;
  max-width: none;
}

.sn-07__intro-sub {
  font-size: clamp(15px,1.7vw,19px);
  line-height: 1.55;
  max-width: 46ch;
}

.sn-07__intro::after {
  content: "Scroll — the bar stays";
  justify-self: center;
  margin-block-start: clamp(20px,4vh,44px);
  padding: 9px 18px;
  border: 1px solid var(--sn-07-line);
  border-radius: 99px;
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11.5px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--sn-07-mut);
  animation: sn-07-cue 2.6s ease-in-out infinite;
}

@keyframes sn-07-cue {
  0%,
    100% {
    transform: translateY(0);
    opacity: .75;
  }

  50% {
    transform: translateY(5px);
    opacity: 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  .sn-07__intro::after {
    animation: none;
  }
}

.sn-07__pagefoot {
  display: grid;
  place-items: center;
  padding: clamp(44px,9vh,110px) 24px;
  border-top: 1px solid var(--sn-07-line);
}

.sn-07__pagefootin {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 12px;
  letter-spacing: .06em;
  color: var(--sn-07-mut);
  text-align: center;
}

.sn-07__intro-title,
.sn-07__intro-sub {
  text-wrap: balance;
}
(() => {
  const root = document.querySelector('.sn-07');
  if (!root || root.dataset.snWired) return;
  root.dataset.snWired = '1';
  const bar = root.querySelector('#sn-07-bar');
  const label = root.querySelector('#sn-07-state');
  let last = 0, ticking = false;
  window.addEventListener('scroll', () => {
    if (ticking) return;
    ticking = true;
    requestAnimationFrame(() => {
      const y = window.scrollY;
      if (Math.abs(y - last) > 6) {
        const hide = y > last && y > 120;
        bar.dataset.hidden = String(hide);
        label.textContent = hide ? 'hidden' : 'visible';
        last = y;
      }
      ticking = false;
    });
  }, { passive: true });
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Sticky Navigation from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Sticky Nav with Directional Hide (Scroll Down) and Reveal (Scroll Up)
Source: https://codefronts.com/navigation/css-sticky-navigation/sticky-nav-with-directional-hide-scroll-down-and-reveal-scroll-up/

Reading-first navigation: the header slides out of the way while the reader moves down the page and snaps back the instant they scroll up, so the nav is always one small gesture away without permanently costing 64px of screen. The demo focuses on the two details that separate a polished implementation from a twitchy one — a movement dead zone and a rest state at the very top.
## HTML
```html
<section class="sn-07" aria-label="Directional hide and reveal sticky nav demo">
    <header class="sn-07__bar" id="sn-07-bar" data-hidden="false">
      <a class="sn-07__brand" href="#sn-07-s1"><span class="sn-07__logo" aria-hidden="true"></span>Dispatch</a>
      <nav class="sn-07__nav" aria-label="Primary">
        <a class="sn-07__link" href="#sn-07-s1" aria-current="page">World</a>
        <a class="sn-07__link" href="#sn-07-s2">Business</a>
        <a class="sn-07__link" href="#sn-07-s3">Culture</a>
      </nav>
      <span class="sn-07__state" id="sn-07-state" aria-live="off">visible</span>
    </header>
  <div class="sn-07__intro">
    <div class="sn-07__introin">
    <p class="sn-07__intro-eyebrow">scroll delta · dead zone · rAF</p>
    <h2 class="sn-07__intro-title">Down it goes, up it comes back</h2>
    <p class="sn-07__intro-sub">Scroll down past the intro, then reverse by a few pixels.</p>
    </div>
  </div>
    <main>
      <section class="sn-07__sec" id="sn-07-s1">
        <p class="sn-07__kicker">World</p>
        <h3 class="sn-07__h">Screen space back to the reader</h3>
        <p class="sn-07__p">A permanently pinned bar costs 64px on every screen of a long article. Directional hiding gives that space back and keeps the nav one gesture away.</p>
      </section>
      <section class="sn-07__sec" id="sn-07-s2">
        <p class="sn-07__kicker">Business</p>
        <h3 class="sn-07__h">The dead zone matters most</h3>
        <p class="sn-07__p">Without a movement threshold the bar flickers on trackpad jitter and elastic bounce. Six pixels is usually the sweet spot.</p>
      </section>
      <section class="sn-07__sec" id="sn-07-s3">
        <p class="sn-07__kicker">Culture</p>
        <h3 class="sn-07__h">Never hide near the top</h3>
        <p class="sn-07__p">Inside the first 120px, hiding reads as a bug. The rest zone keeps the header planted where users expect to find it.</p>
      </section>
      <section class="sn-07__sec">
        <p class="sn-07__kicker">Accessibility</p>
        <h3 class="sn-07__h">Focus pulls it back</h3>
        <p class="sn-07__p">A <code>:focus-within</code> rule overrides the hidden state, so tabbing into the nav can never leave a keyboard user chasing an off-screen link.</p>
      </section>
    </main>
  <footer class="sn-07__pagefoot">
    <p class="sn-07__pagefootin">Sticky navigation pattern 07 of 25 · codefronts.com</p>
  </footer>
</section>
```
## CSS
```css
.sn-07 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: var(--sn-07-bg);
  --sn-07-bg: oklch(0.17 0.02 275);
  --sn-07-surface: oklch(0.215 0.024 275);
  --sn-07-ink: oklch(0.97 0.006 275);
  --sn-07-mut: oklch(0.72 0.018 275);
  --sn-07-acc: oklch(0.82 0.17 85);
  --sn-07-line: oklch(1 0 0/.1);
  --sn-07-h: 62px;
  font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
  color: var(--sn-07-ink);
  -webkit-font-smoothing: antialiased;
}

.sn-07 *,
.sn-07 *::before,
.sn-07 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.sn-07__eyebrow,
.sn-07__intro-eyebrow {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11.5px;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--sn-07-acc);
}

.sn-07__title,
.sn-07__intro-title {
  font-size: clamp(23px,3.5vw,37px);
  line-height: 1.06;
  letter-spacing: -.03em;
  font-weight: 700;
  text-wrap: balance;
}

.sn-07__sub,
.sn-07__intro-sub {
  font-size: 14.4px;
  line-height: 1.6;
  color: var(--sn-07-mut);
  text-wrap: pretty;
}

.sn-07__bar {
  position: sticky;
  top: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  gap: clamp(10px,2vw,22px);
  min-height: var(--sn-07-h);
  padding: 0 clamp(14px,2.4vw,22px);
  background: color-mix(in oklch,var(--sn-07-surface) 84%,transparent);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border-bottom: 1px solid var(--sn-07-line);
  will-change: translate;
  transition: translate .34s cubic-bezier(.32,.72,.3,1);
}

.sn-07__bar[data-hidden="true"] {
  translate: 0 -100%;
}

.sn-07__bar:focus-within {
  translate: 0 0;
}

.sn-07__brand {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -.025em;
  color: inherit;
  text-decoration: none;
}

.sn-07__logo {
  width: 20px;
  height: 20px;
  border-radius: 5px;
  background: var(--sn-07-acc);
  clip-path: polygon(0 0,100% 0,100% 62%,50% 100%,0 62%);
}

.sn-07__nav {
  display: flex;
  align-items: center;
  gap: 2px;
  margin-inline-start: auto;
}

.sn-07__link {
  display: flex;
  align-items: center;
  min-height: 36px;
  padding: 0 12px;
  border-radius: 9px;
  font-size: 13.6px;
  font-weight: 600;
  color: var(--sn-07-mut);
  text-decoration: none;
  transition: color .18s ease,background .18s ease;
}

.sn-07__link:hover {
  color: var(--sn-07-ink);
  background: oklch(1 0 0/.07);
}

.sn-07__link[aria-current="page"] {
  color: var(--sn-07-acc);
}

.sn-07__link:focus-visible,
.sn-07__brand:focus-visible {
  outline: 2px solid var(--sn-07-acc);
  outline-offset: 2px;
}

.sn-07__state {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 10.5px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--sn-07-acc);
  padding: 5px 11px;
  border-radius: 99px;
  border: 1px solid color-mix(in oklch,var(--sn-07-acc) 32%,transparent);
  background: color-mix(in oklch,var(--sn-07-acc) 10%,transparent);
  white-space: nowrap;
}

.sn-07__sec {
  scroll-margin-top: var(--sn-07-h);
  display: grid;
  gap: 10px;
  align-content: center;
  min-height: 66svh;
  padding: clamp(24px,4vw,44px);
  border-bottom: 1px solid var(--sn-07-line);
}

.sn-07__sec:nth-child(even) {
  background: oklch(1 0 0/.025);
}

.sn-07__kicker {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--sn-07-acc);
}

.sn-07__h {
  font-size: clamp(19px,2.6vw,26px);
  line-height: 1.16;
  letter-spacing: -.025em;
  font-weight: 690;
  text-wrap: balance;
}

.sn-07__p {
  max-width: 52ch;
  font-size: 14.8px;
  line-height: 1.64;
  color: var(--sn-07-mut);
  text-wrap: pretty;
}

.sn-07__p code {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: .85em;
  background: oklch(1 0 0/.09);
  padding: 2px 6px;
  border-radius: 5px;
}

@media (max-width: 560px) {
  .sn-07__state {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .sn-07__bar[data-hidden="true"] {
    translate: 0 0;
  }

  .sn-07 * {
    transition-duration: .01ms !important;
  }
}
/* ── full-page mode: the demo IS the document, so sticky pins against the real viewport ── */

:root:has(.sn-07) {
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--sn-07-h) + 10px);
}

@media (prefers-reduced-motion: reduce) {
  :root:has(.sn-07) {
    scroll-behavior: auto;
  }
}

.sn-07__bar,
.sn-07__barin {
  padding-inline: max(clamp(16px,3vw,26px),calc((100% - 1280px)/2));
}

.sn-07__sec {
  padding-inline: max(clamp(22px,5vw,44px),calc((100% - 1120px)/2));
}

.sn-07__intro {
  display: grid;
  place-items: center;
  min-height: min(90svh,900px);
  padding: clamp(56px,12vh,150px) clamp(20px,5vw,48px) clamp(40px,8vh,90px);
  background: radial-gradient(85% 55% at 50% 0%,oklch(0.3 0.06 85/.4),transparent);
}

.sn-07__introin {
  display: grid;
  justify-items: center;
  gap: clamp(12px,1.8vw,18px);
  text-align: center;
  max-width: 26ch;
}

.sn-07__intro-eyebrow {
  font-size: clamp(11px,1.1vw,13px);
}

.sn-07__intro-title {
  font-size: clamp(38px,7.5vw,86px);
  line-height: .98;
  letter-spacing: -.04em;
  max-width: none;
}

.sn-07__intro-sub {
  font-size: clamp(15px,1.7vw,19px);
  line-height: 1.55;
  max-width: 46ch;
}

.sn-07__intro::after {
  content: "Scroll — the bar stays";
  justify-self: center;
  margin-block-start: clamp(20px,4vh,44px);
  padding: 9px 18px;
  border: 1px solid var(--sn-07-line);
  border-radius: 99px;
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11.5px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--sn-07-mut);
  animation: sn-07-cue 2.6s ease-in-out infinite;
}

@keyframes sn-07-cue {
  0%,
    100% {
    transform: translateY(0);
    opacity: .75;
  }

  50% {
    transform: translateY(5px);
    opacity: 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  .sn-07__intro::after {
    animation: none;
  }
}

.sn-07__pagefoot {
  display: grid;
  place-items: center;
  padding: clamp(44px,9vh,110px) 24px;
  border-top: 1px solid var(--sn-07-line);
}

.sn-07__pagefootin {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 12px;
  letter-spacing: .06em;
  color: var(--sn-07-mut);
  text-align: center;
}

.sn-07__intro-title,
.sn-07__intro-sub {
  text-wrap: balance;
}
```

## JavaScript
```js
(() => {
  const root = document.querySelector('.sn-07');
  if (!root || root.dataset.snWired) return;
  root.dataset.snWired = '1';
  const bar = root.querySelector('#sn-07-bar');
  const label = root.querySelector('#sn-07-state');
  let last = 0, ticking = false;
  window.addEventListener('scroll', () => {
    if (ticking) return;
    ticking = true;
    requestAnimationFrame(() => {
      const y = window.scrollY;
      if (Math.abs(y - last) > 6) {
        const hide = y > last && y > 120;
        bar.dataset.hidden = String(hide);
        label.textContent = hide ? 'hidden' : 'visible';
        last = y;
      }
      ticking = false;
    });
  }, { passive: true });
})();
```

How this works

The behaviour is a direction comparison, but a naive version flickers on every trackpad micro-movement. Three guards fix that:

let last = 0, ticking = false;
port.addEventListener('scroll', () => {
  if (ticking) return;                      // 1. one read per frame
  ticking = true;
  requestAnimationFrame(() => {
    const y = window.scrollY;
    if (Math.abs(y - last) > 6) {            // 2. dead zone
      bar.dataset.hidden = String(y > last && y > 120);  // 3. rest zone
      last = y;
    }
    ticking = false;
  });
}, { passive: true });

The rAF guard means the handler runs at most once per painted frame instead of dozens of times per gesture. The 6px dead zone ignores jitter, momentum bounce and the sub-pixel scrolls a trackpad generates when a finger merely rests on it. The 120px rest zone keeps the header permanently visible near the top of the page, where hiding it would feel like a glitch rather than an affordance.

{ passive: true } tells the browser the listener will never call preventDefault(), so scrolling is never blocked waiting on the handler — on a long page that is the difference between smooth scrolling and rubber-banding.

The visual change is a single translate: 0 -100% on the bar. Transform-only animation stays on the compositor, so nothing reflows and the slide holds 60fps even over heavy content. Under prefers-reduced-motion the header simply stays put — motion tied to scrolling is exactly the category of effect that triggers vestibular discomfort.

Make it yours

  • Sensitivity: raise the 6px dead zone for calmer behaviour on touch, lower it for a more responsive desktop feel.
  • Rest zone: the y > 120 guard decides how far a reader must travel before hiding is allowed. Match it to your hero height.
  • Peek instead of full hide: translate: 0 -60% leaves a sliver of the bar visible as a hit target.
  • Hide only on fast scrolls: compare the delta against a velocity threshold (delta divided by elapsed time) instead of a fixed pixel dead zone.

Gotchas — read before shipping

  • An unthrottled scroll handler fires far more often than the screen refreshes and will drop frames. Wrap the work in requestAnimationFrame.
  • Without a dead zone, sub-pixel and elastic-bounce scrolls flip the direction constantly and the header visibly stutters.
  • Animating top triggers layout on every frame. Animate translate or transform, which the compositor handles alone.
  • Hiding the header while focus is inside it strands keyboard users. Add a :focus-within rule that forces the bar back into view.
  • Momentum scrolling on iOS keeps firing after the finger lifts; a threshold that is too tight makes the bar bounce during deceleration.
  • Always honour prefers-reduced-motion — scroll-linked movement is a common trigger for motion sensitivity.

Browser support

ChromeSafariFirefoxEdge
114+17.5+121+114+

Floor set by :has(), oklch(), color-mix(), backdrop-filter, text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.

Everything here is long-established: passive listeners since 2016, requestAnimationFrame since 2013, the individual translate property since Chrome 104 / Safari 14.1 / Firefox 72. Replace translate with transform to reach older engines.

Techniques used in this demo

Search CodeFronts

Loading…