25 CSS Sticky Navigations
19 / 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 Sub-Navigation Bar Locked Under the Main Sticky Header

The second navigation layer that product catalogues and documentation sites need: a filter or category bar that pins directly beneath the main header and stays there while the listing scrolls. Shows the offset arithmetic, the layering, and the visual treatment that keeps two stacked bars from reading as one heavy block.

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-19" aria-label="Sticky sub-navigation under main header demo">
    <header class="sn-19__bar">
      <a class="sn-19__brand" href="#sn-19-s1"><span class="sn-19__logo" aria-hidden="true"></span>Atlas Docs</a>
      <nav class="sn-19__nav" aria-label="Primary">
        <a class="sn-19__link" href="#sn-19-s1" aria-current="page">Guides</a>
        <a class="sn-19__link" href="#sn-19-s2">API</a>
        <a class="sn-19__link" href="#sn-19-s3">Changelog</a>
      </nav>
      <a class="sn-19__cta" href="#sn-19-s2">Console</a>
    </header>
    <div class="sn-19__sub">
      <div class="sn-19__rail" role="group" aria-label="Filter guides by topic">
        <button class="sn-19__chip" type="button" aria-pressed="true">All</button>
        <button class="sn-19__chip" type="button" aria-pressed="false">Getting started</button>
        <button class="sn-19__chip" type="button" aria-pressed="false">Authentication</button>
        <button class="sn-19__chip" type="button" aria-pressed="false">Webhooks</button>
        <button class="sn-19__chip" type="button" aria-pressed="false">Rate limits</button>
        <button class="sn-19__chip" type="button" aria-pressed="false">Migrations</button>
      </div>
    </div>
    <main class="sn-19__main">
      <article class="sn-19__row" id="sn-19-s1"><h3 class="sn-19__rh">Install the SDK</h3><p class="sn-19__rp">Two stacked sticky layers: the header owns the brand and the accent, the sub-nav stays deliberately lighter.</p></article>
      <article class="sn-19__row"><h3 class="sn-19__rh">Authenticate a request</h3><p class="sn-19__rp">The sub-nav's inset is the header's height. Publish it as a token and both layers stay in agreement.</p></article>
      <article class="sn-19__row" id="sn-19-s2"><h3 class="sn-19__rh">Handle webhook retries</h3><p class="sn-19__rp">Chips are buttons with <code>aria-pressed</code> because filtering toggles state — links would promise navigation that never happens.</p></article>
      <article class="sn-19__row"><h3 class="sn-19__rh">Paginate large collections</h3><p class="sn-19__rp">The rail snaps on the inline axis, so a partially visible chip never comes to rest cut in half.</p></article>
      <article class="sn-19__row" id="sn-19-s3"><h3 class="sn-19__rh">Version your integration</h3><p class="sn-19__rp">Scroll padding covers both bars, so deep links land below the sub-nav rather than behind it.</p></article>
      <article class="sn-19__row"><h3 class="sn-19__rh">Migrate from v2</h3><p class="sn-19__rp">Give the secondary layer smaller type and a translucent surface, or the two bars read as one heavy slab.</p></article>
      <article class="sn-19__row"><h3 class="sn-19__rh">Error reference</h3><p class="sn-19__rp">Everything here is two declarations of <code>position:sticky</code> and one piece of arithmetic.</p></article>
    </main>
  <footer class="sn-19__pagefoot">
    <p class="sn-19__pagefootin">Sticky navigation pattern 19 of 25 · codefronts.com</p>
  </footer>
</section>
.sn-19 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: var(--sn-19-bg);
  --sn-19-bg: oklch(0.16 0.02 255);
  --sn-19-surface: oklch(0.205 0.024 255);
  --sn-19-ink: oklch(0.97 0.006 255);
  --sn-19-mut: oklch(0.71 0.018 255);
  --sn-19-acc: oklch(0.78 0.16 200);
  --sn-19-line: oklch(1 0 0/.1);
  --sn-19-h: 60px;
  --sn-19-sub: 52px;
  font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
  color: var(--sn-19-ink);
  -webkit-font-smoothing: antialiased;
}

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

.sn-19__bar {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: clamp(10px,2vw,22px);
  block-size: var(--sn-19-h);
  padding: 0 clamp(14px,2.6vw,22px);
  background: color-mix(in oklch,var(--sn-19-surface) 92%,transparent);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border-bottom: 1px solid var(--sn-19-line);
}

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

.sn-19__logo {
  width: 19px;
  height: 19px;
  border-radius: 50%;
  background: conic-gradient(from 140deg,var(--sn-19-acc),oklch(0.6 0.16 280),var(--sn-19-acc));
}

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

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

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

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

.sn-19__cta {
  display: flex;
  align-items: center;
  min-height: 36px;
  padding: 0 15px;
  border-radius: 9px;
  font-size: 13px;
  font-weight: 660;
  color: oklch(0.16 0.02 255);
  background: var(--sn-19-acc);
  text-decoration: none;
  white-space: nowrap;
}

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

.sn-19__sub {
  position: sticky;
  top: var(--sn-19-h);
  z-index: 20;
  block-size: var(--sn-19-sub);
  background: color-mix(in oklch,var(--sn-19-surface) 88%,transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--sn-19-line);
  container-type: scroll-state;
  container-name: sn-19-sub;
  transition: box-shadow .3s ease;
}

@container sn-19-sub scroll-state(stuck: top) {
  .sn-19__rail {
    box-shadow: 0 14px 26px -24px oklch(0 0 0/.9);
  }
}

.sn-19__rail {
  display: flex;
  align-items: center;
  gap: 7px;
  block-size: 100%;
  padding-inline: clamp(14px,2.6vw,22px);
  overflow-x: auto;
  overscroll-behavior-inline: contain;
  scroll-snap-type: inline mandatory;
  scrollbar-width: none;
  mask-image: linear-gradient(90deg,#000 0,#000 calc(100% - 34px),transparent 100%);
}

.sn-19__rail::-webkit-scrollbar {
  display: none;
}

.sn-19__chip {
  flex: none;
  scroll-snap-align: start;
  display: flex;
  align-items: center;
  min-height: 34px;
  padding: 0 14px;
  border: 1px solid var(--sn-19-line);
  border-radius: 99px;
  background: transparent;
  font-family: inherit;
  font-size: 12.8px;
  font-weight: 600;
  color: var(--sn-19-mut);
  white-space: nowrap;
  cursor: pointer;
  transition: color .18s ease,background .18s ease,border-color .18s ease;
}

.sn-19__chip:hover {
  color: var(--sn-19-ink);
  background: oklch(1 0 0/.06);
}

.sn-19__chip[aria-pressed="true"] {
  color: oklch(0.16 0.02 255);
  background: var(--sn-19-acc);
  border-color: transparent;
}

.sn-19__main {
  display: grid;
}

.sn-19__row {
  scroll-margin-top: calc(var(--sn-19-h) + var(--sn-19-sub));
  display: grid;
  gap: 7px;
  padding: clamp(18px,3vw,26px) clamp(16px,3vw,26px);
  border-bottom: 1px solid var(--sn-19-line);
}

.sn-19__row:nth-child(even) {
  background: oklch(1 0 0/.022);
}

.sn-19__rh {
  font-size: 16.5px;
  line-height: 1.22;
  letter-spacing: -.02em;
  font-weight: 660;
}

.sn-19__rp {
  max-width: 60ch;
  font-size: 14.2px;
  line-height: 1.62;
  color: var(--sn-19-mut);
  text-wrap: pretty;
}

.sn-19__rp 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: 600px) {
  .sn-19__cta {
    display: none;
  }

  .sn-19__link {
    padding: 0 9px;
    font-size: 12.8px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .sn-19 * {
    transition-duration: .01ms !important;
  }
}
/* ── full-page mode: the demo IS the document, so sticky pins against the real viewport ── */

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

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

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

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

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

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

.sn-19__intro-title,
.sn-19__intro-sub {
  text-wrap: balance;
}
(() => {
  const root = document.querySelector('.sn-19');
  if (!root || root.dataset.snWired) return;
  root.dataset.snWired = '1';
  const chips = [...root.querySelectorAll('.sn-19__chip')];
  chips.forEach((chip) => chip.addEventListener('click', () => {
    chips.forEach((c) => c.setAttribute('aria-pressed', String(c === chip)));
  }));
})();
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 Sub-Navigation Bar Locked Under the Main Sticky Header
Source: https://codefronts.com/navigation/css-sticky-navigation/sticky-sub-navigation-bar-locked-under-the-main-sticky-header/

The second navigation layer that product catalogues and documentation sites need: a filter or category bar that pins directly beneath the main header and stays there while the listing scrolls. Shows the offset arithmetic, the layering, and the visual treatment that keeps two stacked bars from reading as one heavy block.
## HTML
```html
<section class="sn-19" aria-label="Sticky sub-navigation under main header demo">
    <header class="sn-19__bar">
      <a class="sn-19__brand" href="#sn-19-s1"><span class="sn-19__logo" aria-hidden="true"></span>Atlas Docs</a>
      <nav class="sn-19__nav" aria-label="Primary">
        <a class="sn-19__link" href="#sn-19-s1" aria-current="page">Guides</a>
        <a class="sn-19__link" href="#sn-19-s2">API</a>
        <a class="sn-19__link" href="#sn-19-s3">Changelog</a>
      </nav>
      <a class="sn-19__cta" href="#sn-19-s2">Console</a>
    </header>
    <div class="sn-19__sub">
      <div class="sn-19__rail" role="group" aria-label="Filter guides by topic">
        <button class="sn-19__chip" type="button" aria-pressed="true">All</button>
        <button class="sn-19__chip" type="button" aria-pressed="false">Getting started</button>
        <button class="sn-19__chip" type="button" aria-pressed="false">Authentication</button>
        <button class="sn-19__chip" type="button" aria-pressed="false">Webhooks</button>
        <button class="sn-19__chip" type="button" aria-pressed="false">Rate limits</button>
        <button class="sn-19__chip" type="button" aria-pressed="false">Migrations</button>
      </div>
    </div>
    <main class="sn-19__main">
      <article class="sn-19__row" id="sn-19-s1"><h3 class="sn-19__rh">Install the SDK</h3><p class="sn-19__rp">Two stacked sticky layers: the header owns the brand and the accent, the sub-nav stays deliberately lighter.</p></article>
      <article class="sn-19__row"><h3 class="sn-19__rh">Authenticate a request</h3><p class="sn-19__rp">The sub-nav's inset is the header's height. Publish it as a token and both layers stay in agreement.</p></article>
      <article class="sn-19__row" id="sn-19-s2"><h3 class="sn-19__rh">Handle webhook retries</h3><p class="sn-19__rp">Chips are buttons with <code>aria-pressed</code> because filtering toggles state — links would promise navigation that never happens.</p></article>
      <article class="sn-19__row"><h3 class="sn-19__rh">Paginate large collections</h3><p class="sn-19__rp">The rail snaps on the inline axis, so a partially visible chip never comes to rest cut in half.</p></article>
      <article class="sn-19__row" id="sn-19-s3"><h3 class="sn-19__rh">Version your integration</h3><p class="sn-19__rp">Scroll padding covers both bars, so deep links land below the sub-nav rather than behind it.</p></article>
      <article class="sn-19__row"><h3 class="sn-19__rh">Migrate from v2</h3><p class="sn-19__rp">Give the secondary layer smaller type and a translucent surface, or the two bars read as one heavy slab.</p></article>
      <article class="sn-19__row"><h3 class="sn-19__rh">Error reference</h3><p class="sn-19__rp">Everything here is two declarations of <code>position:sticky</code> and one piece of arithmetic.</p></article>
    </main>
  <footer class="sn-19__pagefoot">
    <p class="sn-19__pagefootin">Sticky navigation pattern 19 of 25 · codefronts.com</p>
  </footer>
</section>
```
## CSS
```css
.sn-19 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: var(--sn-19-bg);
  --sn-19-bg: oklch(0.16 0.02 255);
  --sn-19-surface: oklch(0.205 0.024 255);
  --sn-19-ink: oklch(0.97 0.006 255);
  --sn-19-mut: oklch(0.71 0.018 255);
  --sn-19-acc: oklch(0.78 0.16 200);
  --sn-19-line: oklch(1 0 0/.1);
  --sn-19-h: 60px;
  --sn-19-sub: 52px;
  font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
  color: var(--sn-19-ink);
  -webkit-font-smoothing: antialiased;
}

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

.sn-19__bar {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: clamp(10px,2vw,22px);
  block-size: var(--sn-19-h);
  padding: 0 clamp(14px,2.6vw,22px);
  background: color-mix(in oklch,var(--sn-19-surface) 92%,transparent);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border-bottom: 1px solid var(--sn-19-line);
}

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

.sn-19__logo {
  width: 19px;
  height: 19px;
  border-radius: 50%;
  background: conic-gradient(from 140deg,var(--sn-19-acc),oklch(0.6 0.16 280),var(--sn-19-acc));
}

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

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

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

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

.sn-19__cta {
  display: flex;
  align-items: center;
  min-height: 36px;
  padding: 0 15px;
  border-radius: 9px;
  font-size: 13px;
  font-weight: 660;
  color: oklch(0.16 0.02 255);
  background: var(--sn-19-acc);
  text-decoration: none;
  white-space: nowrap;
}

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

.sn-19__sub {
  position: sticky;
  top: var(--sn-19-h);
  z-index: 20;
  block-size: var(--sn-19-sub);
  background: color-mix(in oklch,var(--sn-19-surface) 88%,transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--sn-19-line);
  container-type: scroll-state;
  container-name: sn-19-sub;
  transition: box-shadow .3s ease;
}

@container sn-19-sub scroll-state(stuck: top) {
  .sn-19__rail {
    box-shadow: 0 14px 26px -24px oklch(0 0 0/.9);
  }
}

.sn-19__rail {
  display: flex;
  align-items: center;
  gap: 7px;
  block-size: 100%;
  padding-inline: clamp(14px,2.6vw,22px);
  overflow-x: auto;
  overscroll-behavior-inline: contain;
  scroll-snap-type: inline mandatory;
  scrollbar-width: none;
  mask-image: linear-gradient(90deg,#000 0,#000 calc(100% - 34px),transparent 100%);
}

.sn-19__rail::-webkit-scrollbar {
  display: none;
}

.sn-19__chip {
  flex: none;
  scroll-snap-align: start;
  display: flex;
  align-items: center;
  min-height: 34px;
  padding: 0 14px;
  border: 1px solid var(--sn-19-line);
  border-radius: 99px;
  background: transparent;
  font-family: inherit;
  font-size: 12.8px;
  font-weight: 600;
  color: var(--sn-19-mut);
  white-space: nowrap;
  cursor: pointer;
  transition: color .18s ease,background .18s ease,border-color .18s ease;
}

.sn-19__chip:hover {
  color: var(--sn-19-ink);
  background: oklch(1 0 0/.06);
}

.sn-19__chip[aria-pressed="true"] {
  color: oklch(0.16 0.02 255);
  background: var(--sn-19-acc);
  border-color: transparent;
}

.sn-19__main {
  display: grid;
}

.sn-19__row {
  scroll-margin-top: calc(var(--sn-19-h) + var(--sn-19-sub));
  display: grid;
  gap: 7px;
  padding: clamp(18px,3vw,26px) clamp(16px,3vw,26px);
  border-bottom: 1px solid var(--sn-19-line);
}

.sn-19__row:nth-child(even) {
  background: oklch(1 0 0/.022);
}

.sn-19__rh {
  font-size: 16.5px;
  line-height: 1.22;
  letter-spacing: -.02em;
  font-weight: 660;
}

.sn-19__rp {
  max-width: 60ch;
  font-size: 14.2px;
  line-height: 1.62;
  color: var(--sn-19-mut);
  text-wrap: pretty;
}

.sn-19__rp 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: 600px) {
  .sn-19__cta {
    display: none;
  }

  .sn-19__link {
    padding: 0 9px;
    font-size: 12.8px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .sn-19 * {
    transition-duration: .01ms !important;
  }
}
/* ── full-page mode: the demo IS the document, so sticky pins against the real viewport ── */

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

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

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

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

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

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

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

## JavaScript
```js
(() => {
  const root = document.querySelector('.sn-19');
  if (!root || root.dataset.snWired) return;
  root.dataset.snWired = '1';
  const chips = [...root.querySelectorAll('.sn-19__chip')];
  chips.forEach((chip) => chip.addEventListener('click', () => {
    chips.forEach((c) => c.setAttribute('aria-pressed', String(c === chip)));
  }));
})();
```

How this works

The sub-nav is a sticky element whose inset is the header's height:

.header { position:sticky; top:0;                z-index:30 }
.subnav { position:sticky; top:var(--header-h);  z-index:20 }
.scroller{ scroll-padding-top: calc(var(--header-h) + var(--sub-h)) }

Arithmetic aside, the design problem is weight. Two full-strength bars stacked look like a 110px slab of chrome. The fix is hierarchy: the primary header keeps the brand, the accent and the solid background; the sub-nav is lighter — smaller type, a translucent surface, no shadow of its own — so the eye reads one header with a secondary row rather than two competing bars.

The filter chips are the sub-nav's content. Each is a real <button> with aria-pressed, since filtering is a toggle rather than navigation; if your sub-nav links to different pages, use anchors with aria-current="page" instead. The rail scrolls horizontally with scroll-snap-type: inline mandatory so chips always come to rest on a boundary rather than half-cut — the detail that makes an overflowing filter row feel native on touch.

The sub-nav gains its bottom shadow only when it is actually overlapping content, using the same scroll-state technique as demo 08, so the resting state stays flat and light.

Make it yours

  • Sub-nav that scrolls away: drop its position:sticky and it becomes a normal in-flow row that leaves with the hero.
  • Counts in chips: append a muted number span — pair it with the result count so the filter state is legible without applying it.
  • Segmented control look: wrap the chips in a padded track with a shared background and one rounded highlight behind the active chip.
  • Sortable list header: the same layer works as a sticky table header row — give it display:grid matching the table's columns.

Gotchas — read before shipping

  • The sub-nav's inset must equal the header's rendered height, not its declared one. If the header can wrap or shrink, publish the measured height to a custom property.
  • Both bars need explicit z-index, with the upper bar higher, or the sub-nav paints over the header as they meet.
  • Two heavy bars stacked eat a third of a phone screen. Make the secondary layer visually lighter and shorter.
  • Filter chips are toggles, not links: use <button aria-pressed>. Reserve aria-current for chips that navigate.
  • A horizontally scrolling chip rail with no visible affordance hides its own overflow. Fade the trailing edge with a mask so more content is discoverable.
  • Anchor scroll padding must account for both bars, otherwise headings land behind the sub-nav.

Browser support

ChromeSafariFirefoxEdge
114+17.5+121+114+

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

Sticky layering and scroll-snap are universally supported (snap since 2019). The optional scroll-state shadow needs Chrome 133 or Safari 26 and is purely additive.

Techniques used in this demo

Search CodeFronts

Loading…