25 CSS Sticky Navigations
16 / 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 Header with Multi-Column Mega Menu Dropdown

Category navigation for a catalogue that does not fit in a list: a full-width panel with grouped columns and a promoted product, opening from a sticky header so it stays reachable at any scroll depth. Built as a disclosure rather than a menu, which is what lets it work identically for pointer, keyboard and touch.

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-16" aria-label="Multi-column mega menu demo">
    <header class="sn-16__bar" id="sn-16-bar">
      <a class="sn-16__brand" href="#sn-16-s1"><span class="sn-16__logo" aria-hidden="true"></span>Forge&amp;Field</a>
      <nav class="sn-16__nav" aria-label="Primary">
        <button class="sn-16__trigger" type="button" id="sn-16-trigger" aria-expanded="false" aria-controls="sn-16-mega">
          Shop
          <svg class="sn-16__caret" viewBox="0 0 12 12" aria-hidden="true" focusable="false"><path d="M2.5 4.5 6 8l3.5-3.5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
        </button>
        <a class="sn-16__link" href="#sn-16-s2">Workshop</a>
        <a class="sn-16__link" href="#sn-16-s3">Stories</a>
      </nav>
      <a class="sn-16__cta" href="#sn-16-s3">Bag</a>
      <div class="sn-16__mega" id="sn-16-mega" hidden>
        <div class="sn-16__col">
          <h3 class="sn-16__ch">Hand tools</h3>
          <a class="sn-16__mlink" href="#sn-16-s1">Chisels &amp; gouges</a>
          <a class="sn-16__mlink" href="#sn-16-s1">Planes</a>
          <a class="sn-16__mlink" href="#sn-16-s1">Marking &amp; measuring</a>
          <a class="sn-16__mlink" href="#sn-16-s1">Sharpening</a>
        </div>
        <div class="sn-16__col">
          <h3 class="sn-16__ch">Workshop</h3>
          <a class="sn-16__mlink" href="#sn-16-s2">Benches</a>
          <a class="sn-16__mlink" href="#sn-16-s2">Vices &amp; clamps</a>
          <a class="sn-16__mlink" href="#sn-16-s2">Dust extraction</a>
          <a class="sn-16__mlink" href="#sn-16-s2">Storage</a>
        </div>
        <div class="sn-16__col">
          <h3 class="sn-16__ch">Materials</h3>
          <a class="sn-16__mlink" href="#sn-16-s3">Hardwoods</a>
          <a class="sn-16__mlink" href="#sn-16-s3">Finishes &amp; oils</a>
          <a class="sn-16__mlink" href="#sn-16-s3">Adhesives</a>
          <a class="sn-16__mlink" href="#sn-16-s3">Abrasives</a>
        </div>
        <a class="sn-16__promo" href="#sn-16-s1">
          <img class="sn-16__promoimg" src="https://images.unsplash.com/photo-1504148455328-c376907d081c?q=80&w=800&auto=format&fit=crop" alt="Woodworking hand tools arranged on a workbench" width="800" height="600" loading="lazy" decoding="async">
          <span class="sn-16__promotag">New season</span>
          <span class="sn-16__promoh">The bench collection</span>
        </a>
      </div>
    </header>
    <main>
      <section class="sn-16__sec" id="sn-16-s1">
        <p class="sn-16__kicker">Disclosure, not menu</p>
        <h3 class="sn-16__h">Links should behave like links</h3>
        <p class="sn-16__p">An ARIA menu changes the keyboard contract to arrow-keys-only and announces items as application commands. A disclosure keeps Tab, keeps link semantics and works on touch.</p>
      </section>
      <section class="sn-16__sec" id="sn-16-s2">
        <p class="sn-16__kicker">Grace period</p>
        <h3 class="sn-16__h">Diagonal mouse paths</h3>
        <p class="sn-16__p">Closing the instant the pointer leaves the trigger breaks the natural diagonal travel toward the panel. A short delay makes it feel forgiving without feeling stuck.</p>
      </section>
      <section class="sn-16__sec" id="sn-16-s3">
        <p class="sn-16__kicker">Sticky context</p>
        <h3 class="sn-16__h">The panel travels with the bar</h3>
        <p class="sn-16__p">Absolutely positioned inside the sticky header, the panel needs no coordinate maths — it is pinned wherever the header is.</p>
      </section>
    </main>
  <footer class="sn-16__pagefoot">
    <p class="sn-16__pagefootin">Sticky navigation pattern 16 of 25 · codefronts.com</p>
  </footer>
</section>
.sn-16 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: var(--sn-16-bg);
  --sn-16-bg: oklch(0.95 0.012 75);
  --sn-16-surface: oklch(0.995 0.004 75);
  --sn-16-ink: oklch(0.23 0.024 55);
  --sn-16-mut: oklch(0.53 0.022 55);
  --sn-16-acc: oklch(0.5 0.13 42);
  --sn-16-line: oklch(0.23 0.024 55/.13);
  --sn-16-h: 66px;
  font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
  color: var(--sn-16-ink);
  -webkit-font-smoothing: antialiased;
}

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

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

.sn-16__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-16__logo {
  width: 20px;
  height: 20px;
  border-radius: 5px;
  background: var(--sn-16-acc);
  clip-path: polygon(0 20%,100% 0,80% 100%,20% 80%);
}

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

.sn-16__trigger {
  display: flex;
  align-items: center;
  gap: 6px;
  min-height: 38px;
  padding: 0 12px;
  border: 0;
  border-radius: 9px;
  background: transparent;
  font-family: inherit;
  font-size: 13.6px;
  font-weight: 640;
  color: var(--sn-16-mut);
  cursor: pointer;
  transition: color .18s ease,background .18s ease;
}

.sn-16__trigger:hover,
.sn-16__trigger[aria-expanded="true"] {
  color: var(--sn-16-ink);
  background: oklch(0.23 0.024 55/.06);
}

.sn-16__caret {
  inline-size: 12px;
  block-size: 12px;
  transition: rotate .26s ease;
}

.sn-16__trigger[aria-expanded="true"] .sn-16__caret {
  rotate: 180deg;
}

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

.sn-16__link:hover {
  color: var(--sn-16-ink);
  background: oklch(0.23 0.024 55/.05);
}

.sn-16__cta {
  display: flex;
  align-items: center;
  min-height: 38px;
  padding: 0 16px;
  border-radius: 99px;
  font-size: 13.2px;
  font-weight: 660;
  color: oklch(1 0 0);
  background: var(--sn-16-acc);
  text-decoration: none;
  white-space: nowrap;
  transition: transform .18s ease;
}

.sn-16__cta:hover {
  transform: translateY(-1px);
}

.sn-16__trigger:focus-visible,
.sn-16__link:focus-visible,
.sn-16__cta:focus-visible,
.sn-16__brand:focus-visible,
.sn-16__mlink:focus-visible,
.sn-16__promo:focus-visible {
  outline: 2px solid var(--sn-16-acc);
  outline-offset: 2px;
}

.sn-16__mega {
  position: absolute;
  inset-inline: 0;
  top: 100%;
  display: grid;
  grid-template-columns: repeat(3,minmax(0,1fr)) 250px;
  gap: clamp(16px,2.6vw,32px);
  padding: clamp(20px,3vw,30px);
  background: var(--sn-16-surface);
  border-bottom: 1px solid var(--sn-16-line);
  box-shadow: 0 30px 44px -30px oklch(0.23 0.024 55/.5);
  max-block-size: min(62svh,420px);
  overflow: auto;
  opacity: 1;
  translate: 0 0;
  transition: opacity .22s ease,translate .26s cubic-bezier(.32,.72,.3,1);
}

@starting-style {
  .sn-16__mega {
    opacity: 0;
    translate: 0 -10px;
  }
}

.sn-16__mega[hidden] {
  display: none;
}

.sn-16__col {
  display: grid;
  align-content: start;
  gap: 3px;
  min-width: 0;
}

.sn-16__ch {
  margin-block-end: 7px;
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 10.5px;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--sn-16-acc);
}

.sn-16__mlink {
  display: flex;
  align-items: center;
  min-height: 38px;
  padding: 0 10px;
  margin-inline: -10px;
  border-radius: 8px;
  font-size: 14px;
  font-weight: 560;
  color: var(--sn-16-mut);
  text-decoration: none;
  transition: color .16s ease,background .16s ease;
}

.sn-16__mlink:hover {
  color: var(--sn-16-ink);
  background: oklch(0.23 0.024 55/.05);
}

.sn-16__promo {
  position: relative;
  display: grid;
  align-content: end;
  gap: 4px;
  min-block-size: 160px;
  padding: 16px;
  border-radius: 12px;
  overflow: hidden;
  color: oklch(1 0 0);
  text-decoration: none;
  background: linear-gradient(160deg,oklch(0.5 0.1 45),oklch(0.35 0.06 60));
}

.sn-16__promoimg {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  opacity: .62;
  transition: scale .5s cubic-bezier(.32,.72,.3,1);
}

.sn-16__promo:hover .sn-16__promoimg {
  scale: 1.06;
}

.sn-16__promotag,
.sn-16__promoh {
  position: relative;
}

.sn-16__promotag {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 10px;
  letter-spacing: .14em;
  text-transform: uppercase;
  opacity: .92;
}

.sn-16__promoh {
  font-size: 17px;
  font-weight: 680;
  letter-spacing: -.02em;
  text-shadow: 0 1px 12px oklch(0 0 0/.5);
}

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

.sn-16__sec:nth-child(even) {
  background: oklch(0.975 0.008 70);
}

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

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

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

@media (max-width: 780px) {
  .sn-16__mega {
    grid-template-columns: repeat(2,minmax(0,1fr));
  }

  .sn-16__promo {
    grid-column: 1/-1;
    min-block-size: 120px;
  }
}

@media (max-width: 520px) {
  .sn-16__mega {
    grid-template-columns: 1fr;
  }

  .sn-16__link {
    display: none;
  }
}

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

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

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

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

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

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

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

.sn-16__intro-title,
.sn-16__intro-sub {
  text-wrap: balance;
}
(() => {
  const root = document.querySelector('.sn-16');
  if (!root || root.dataset.snWired) return;
  root.dataset.snWired = '1';
  const bar = root.querySelector('#sn-16-bar');
  const trigger = root.querySelector('#sn-16-trigger');
  const mega = root.querySelector('#sn-16-mega');
  let timer;
  const set = (open) => {
    clearTimeout(timer);
    trigger.setAttribute('aria-expanded', String(open));
    mega.hidden = !open;
  };
  trigger.addEventListener('click', () => set(trigger.getAttribute('aria-expanded') !== 'true'));
  trigger.addEventListener('pointerenter', () => set(true));
  bar.addEventListener('focusin', (e) => { if (e.target === trigger) set(true); });
  bar.addEventListener('pointerleave', () => { timer = setTimeout(() => set(false), 160); });
  bar.addEventListener('pointerenter', () => clearTimeout(timer));
  bar.addEventListener('focusout', (e) => { if (!bar.contains(e.relatedTarget)) set(false); });
  root.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && trigger.getAttribute('aria-expanded') === 'true') { set(false); trigger.focus(); }
  });
  mega.addEventListener('click', (e) => { if (e.target.closest('a')) set(false); });
})();
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 Header with Multi-Column Mega Menu Dropdown
Source: https://codefronts.com/navigation/css-sticky-navigation/sticky-header-with-multi-column-mega-menu-dropdown/

Category navigation for a catalogue that does not fit in a list: a full-width panel with grouped columns and a promoted product, opening from a sticky header so it stays reachable at any scroll depth. Built as a disclosure rather than a menu, which is what lets it work identically for pointer, keyboard and touch.
## HTML
```html
<section class="sn-16" aria-label="Multi-column mega menu demo">
    <header class="sn-16__bar" id="sn-16-bar">
      <a class="sn-16__brand" href="#sn-16-s1"><span class="sn-16__logo" aria-hidden="true"></span>Forge&amp;Field</a>
      <nav class="sn-16__nav" aria-label="Primary">
        <button class="sn-16__trigger" type="button" id="sn-16-trigger" aria-expanded="false" aria-controls="sn-16-mega">
          Shop
          <svg class="sn-16__caret" viewBox="0 0 12 12" aria-hidden="true" focusable="false"><path d="M2.5 4.5 6 8l3.5-3.5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
        </button>
        <a class="sn-16__link" href="#sn-16-s2">Workshop</a>
        <a class="sn-16__link" href="#sn-16-s3">Stories</a>
      </nav>
      <a class="sn-16__cta" href="#sn-16-s3">Bag</a>
      <div class="sn-16__mega" id="sn-16-mega" hidden>
        <div class="sn-16__col">
          <h3 class="sn-16__ch">Hand tools</h3>
          <a class="sn-16__mlink" href="#sn-16-s1">Chisels &amp; gouges</a>
          <a class="sn-16__mlink" href="#sn-16-s1">Planes</a>
          <a class="sn-16__mlink" href="#sn-16-s1">Marking &amp; measuring</a>
          <a class="sn-16__mlink" href="#sn-16-s1">Sharpening</a>
        </div>
        <div class="sn-16__col">
          <h3 class="sn-16__ch">Workshop</h3>
          <a class="sn-16__mlink" href="#sn-16-s2">Benches</a>
          <a class="sn-16__mlink" href="#sn-16-s2">Vices &amp; clamps</a>
          <a class="sn-16__mlink" href="#sn-16-s2">Dust extraction</a>
          <a class="sn-16__mlink" href="#sn-16-s2">Storage</a>
        </div>
        <div class="sn-16__col">
          <h3 class="sn-16__ch">Materials</h3>
          <a class="sn-16__mlink" href="#sn-16-s3">Hardwoods</a>
          <a class="sn-16__mlink" href="#sn-16-s3">Finishes &amp; oils</a>
          <a class="sn-16__mlink" href="#sn-16-s3">Adhesives</a>
          <a class="sn-16__mlink" href="#sn-16-s3">Abrasives</a>
        </div>
        <a class="sn-16__promo" href="#sn-16-s1">
          <img class="sn-16__promoimg" src="https://images.unsplash.com/photo-1504148455328-c376907d081c?q=80&w=800&auto=format&fit=crop" alt="Woodworking hand tools arranged on a workbench" width="800" height="600" loading="lazy" decoding="async">
          <span class="sn-16__promotag">New season</span>
          <span class="sn-16__promoh">The bench collection</span>
        </a>
      </div>
    </header>
    <main>
      <section class="sn-16__sec" id="sn-16-s1">
        <p class="sn-16__kicker">Disclosure, not menu</p>
        <h3 class="sn-16__h">Links should behave like links</h3>
        <p class="sn-16__p">An ARIA menu changes the keyboard contract to arrow-keys-only and announces items as application commands. A disclosure keeps Tab, keeps link semantics and works on touch.</p>
      </section>
      <section class="sn-16__sec" id="sn-16-s2">
        <p class="sn-16__kicker">Grace period</p>
        <h3 class="sn-16__h">Diagonal mouse paths</h3>
        <p class="sn-16__p">Closing the instant the pointer leaves the trigger breaks the natural diagonal travel toward the panel. A short delay makes it feel forgiving without feeling stuck.</p>
      </section>
      <section class="sn-16__sec" id="sn-16-s3">
        <p class="sn-16__kicker">Sticky context</p>
        <h3 class="sn-16__h">The panel travels with the bar</h3>
        <p class="sn-16__p">Absolutely positioned inside the sticky header, the panel needs no coordinate maths — it is pinned wherever the header is.</p>
      </section>
    </main>
  <footer class="sn-16__pagefoot">
    <p class="sn-16__pagefootin">Sticky navigation pattern 16 of 25 · codefronts.com</p>
  </footer>
</section>
```
## CSS
```css
.sn-16 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: var(--sn-16-bg);
  --sn-16-bg: oklch(0.95 0.012 75);
  --sn-16-surface: oklch(0.995 0.004 75);
  --sn-16-ink: oklch(0.23 0.024 55);
  --sn-16-mut: oklch(0.53 0.022 55);
  --sn-16-acc: oklch(0.5 0.13 42);
  --sn-16-line: oklch(0.23 0.024 55/.13);
  --sn-16-h: 66px;
  font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
  color: var(--sn-16-ink);
  -webkit-font-smoothing: antialiased;
}

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

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

.sn-16__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-16__logo {
  width: 20px;
  height: 20px;
  border-radius: 5px;
  background: var(--sn-16-acc);
  clip-path: polygon(0 20%,100% 0,80% 100%,20% 80%);
}

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

.sn-16__trigger {
  display: flex;
  align-items: center;
  gap: 6px;
  min-height: 38px;
  padding: 0 12px;
  border: 0;
  border-radius: 9px;
  background: transparent;
  font-family: inherit;
  font-size: 13.6px;
  font-weight: 640;
  color: var(--sn-16-mut);
  cursor: pointer;
  transition: color .18s ease,background .18s ease;
}

.sn-16__trigger:hover,
.sn-16__trigger[aria-expanded="true"] {
  color: var(--sn-16-ink);
  background: oklch(0.23 0.024 55/.06);
}

.sn-16__caret {
  inline-size: 12px;
  block-size: 12px;
  transition: rotate .26s ease;
}

.sn-16__trigger[aria-expanded="true"] .sn-16__caret {
  rotate: 180deg;
}

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

.sn-16__link:hover {
  color: var(--sn-16-ink);
  background: oklch(0.23 0.024 55/.05);
}

.sn-16__cta {
  display: flex;
  align-items: center;
  min-height: 38px;
  padding: 0 16px;
  border-radius: 99px;
  font-size: 13.2px;
  font-weight: 660;
  color: oklch(1 0 0);
  background: var(--sn-16-acc);
  text-decoration: none;
  white-space: nowrap;
  transition: transform .18s ease;
}

.sn-16__cta:hover {
  transform: translateY(-1px);
}

.sn-16__trigger:focus-visible,
.sn-16__link:focus-visible,
.sn-16__cta:focus-visible,
.sn-16__brand:focus-visible,
.sn-16__mlink:focus-visible,
.sn-16__promo:focus-visible {
  outline: 2px solid var(--sn-16-acc);
  outline-offset: 2px;
}

.sn-16__mega {
  position: absolute;
  inset-inline: 0;
  top: 100%;
  display: grid;
  grid-template-columns: repeat(3,minmax(0,1fr)) 250px;
  gap: clamp(16px,2.6vw,32px);
  padding: clamp(20px,3vw,30px);
  background: var(--sn-16-surface);
  border-bottom: 1px solid var(--sn-16-line);
  box-shadow: 0 30px 44px -30px oklch(0.23 0.024 55/.5);
  max-block-size: min(62svh,420px);
  overflow: auto;
  opacity: 1;
  translate: 0 0;
  transition: opacity .22s ease,translate .26s cubic-bezier(.32,.72,.3,1);
}

@starting-style {
  .sn-16__mega {
    opacity: 0;
    translate: 0 -10px;
  }
}

.sn-16__mega[hidden] {
  display: none;
}

.sn-16__col {
  display: grid;
  align-content: start;
  gap: 3px;
  min-width: 0;
}

.sn-16__ch {
  margin-block-end: 7px;
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 10.5px;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--sn-16-acc);
}

.sn-16__mlink {
  display: flex;
  align-items: center;
  min-height: 38px;
  padding: 0 10px;
  margin-inline: -10px;
  border-radius: 8px;
  font-size: 14px;
  font-weight: 560;
  color: var(--sn-16-mut);
  text-decoration: none;
  transition: color .16s ease,background .16s ease;
}

.sn-16__mlink:hover {
  color: var(--sn-16-ink);
  background: oklch(0.23 0.024 55/.05);
}

.sn-16__promo {
  position: relative;
  display: grid;
  align-content: end;
  gap: 4px;
  min-block-size: 160px;
  padding: 16px;
  border-radius: 12px;
  overflow: hidden;
  color: oklch(1 0 0);
  text-decoration: none;
  background: linear-gradient(160deg,oklch(0.5 0.1 45),oklch(0.35 0.06 60));
}

.sn-16__promoimg {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  opacity: .62;
  transition: scale .5s cubic-bezier(.32,.72,.3,1);
}

.sn-16__promo:hover .sn-16__promoimg {
  scale: 1.06;
}

.sn-16__promotag,
.sn-16__promoh {
  position: relative;
}

.sn-16__promotag {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 10px;
  letter-spacing: .14em;
  text-transform: uppercase;
  opacity: .92;
}

.sn-16__promoh {
  font-size: 17px;
  font-weight: 680;
  letter-spacing: -.02em;
  text-shadow: 0 1px 12px oklch(0 0 0/.5);
}

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

.sn-16__sec:nth-child(even) {
  background: oklch(0.975 0.008 70);
}

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

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

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

@media (max-width: 780px) {
  .sn-16__mega {
    grid-template-columns: repeat(2,minmax(0,1fr));
  }

  .sn-16__promo {
    grid-column: 1/-1;
    min-block-size: 120px;
  }
}

@media (max-width: 520px) {
  .sn-16__mega {
    grid-template-columns: 1fr;
  }

  .sn-16__link {
    display: none;
  }
}

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

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

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

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

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

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

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

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

## JavaScript
```js
(() => {
  const root = document.querySelector('.sn-16');
  if (!root || root.dataset.snWired) return;
  root.dataset.snWired = '1';
  const bar = root.querySelector('#sn-16-bar');
  const trigger = root.querySelector('#sn-16-trigger');
  const mega = root.querySelector('#sn-16-mega');
  let timer;
  const set = (open) => {
    clearTimeout(timer);
    trigger.setAttribute('aria-expanded', String(open));
    mega.hidden = !open;
  };
  trigger.addEventListener('click', () => set(trigger.getAttribute('aria-expanded') !== 'true'));
  trigger.addEventListener('pointerenter', () => set(true));
  bar.addEventListener('focusin', (e) => { if (e.target === trigger) set(true); });
  bar.addEventListener('pointerleave', () => { timer = setTimeout(() => set(false), 160); });
  bar.addEventListener('pointerenter', () => clearTimeout(timer));
  bar.addEventListener('focusout', (e) => { if (!bar.contains(e.relatedTarget)) set(false); });
  root.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && trigger.getAttribute('aria-expanded') === 'true') { set(false); trigger.focus(); }
  });
  mega.addEventListener('click', (e) => { if (e.target.closest('a')) set(false); });
})();
```

How this works

The panel is a grid that spans the header's full width and drops beneath it:

.mega{
  position:absolute; inset-inline:0; top:100%;
  display:grid;
  grid-template-columns: repeat(3, minmax(0,1fr)) 260px;   /* 3 lists + promo */
  gap: 28px;
}
.mega[hidden]{ display:none }

@starting-style{ .mega{ opacity:0; translate:0 -8px } }   /* entry from nothing */

Because the trigger sits in a sticky header, the panel inherits the header's stacking context and stays pinned with it — no fixed positioning and no manual coordinate maths as the page scrolls.

The interaction model is the important decision. This is not a role="menu": ARIA menus imply desktop-application semantics where arrow keys are the only way to move and Tab exits the whole widget, which is wrong for a page of links. Treating it as a disclosure — a <button aria-expanded> revealing a region of ordinary links — means Tab walks the links naturally, screen readers announce them as links, and touch users get a straightforward tap.

Hover opens it for pointer users, but hover alone would strand keyboards, so the same open state is driven by focusin and by click. Closing is handled three ways: Escape, focus leaving the header, and pointer leaving with a short grace delay so a diagonal mouse path across the gap does not slam the panel shut mid-travel.

Make it yours

  • Column count: one grid-template-columns value. Drop the promo track for a pure link matrix, or make it two promos on wide screens.
  • Per-category panels: give each trigger its own panel id and reuse the same script — it keys off aria-controls.
  • Full-bleed backdrop: extend the panel to the viewport edges and centre its content on the page grid from demo 04.
  • Anchor positioning: with anchor-name and position-anchor the panel can tether to its trigger instead of the header, which suits menus that are not full width.

Gotchas — read before shipping

  • role="menu" on a set of navigation links makes screen readers announce them as application menu items and changes the expected keys. Use a disclosure button plus a plain link list.
  • Hover-only mega menus are unusable by keyboard and unreliable on touch, where the first tap becomes a hover and the second follows the link.
  • Closing instantly on pointerleave makes diagonal mouse paths fail. A 120–200ms grace period fixes it without feeling sticky.
  • A panel hidden with opacity:0 alone keeps its links focusable. Use the hidden attribute or visibility.
  • An absolutely-positioned panel inside an ancestor with overflow:hidden gets clipped — a very common failure when the header lives inside a wrapper with a rounded radius.
  • Long category lists inside a sticky header can exceed the viewport height. Cap the panel with max-block-size and let it scroll internally.

Browser support

ChromeSafariFirefoxEdge
117+17.5+129+117+

@starting-style shipped in Chrome 117, Safari 17.5 and Firefox 129; where it is missing the panel simply appears without its entry animation. Everything else here is universally supported.

Techniques used in this demo

Search CodeFronts

Loading…