html|css|javascript 9 views

Responsive Sticky Header Navigation

Create a responsive sticky header that adjusts upon scrolling for improved navigation experience.

By TWC Team • Mar 09, 2026

Code

<!--
  Responsive Sticky Header Navigation
  Usage: Paste into an HTML file. Header sticks to top and updates style/size on scroll; includes accessible mobile menu.
-->
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>Responsive Sticky Header Navigation</title>
  <style>
    :root { --bg: #fff; --fg: #111; --shadow: 0 2px 14px rgba(0,0,0,.08); --ring: 0 0 0 3px rgba(59,130,246,.35); }
    * { box-sizing: border-box; }
    body { margin: 0; font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; color: var(--fg); min-height: 200vh; }
    a { color: inherit; }

    .header {
      position: sticky; top: 0; z-index: 1000;
      background: var(--bg); box-shadow: var(--shadow);
      transition: background-color .25s ease, color .25s ease, padding .25s ease, box-shadow .25s ease;
      padding: 14px 16px;
    }
    .header.scrolled {
      background-color: rgba(0,0,0,.82); color: #fff;
      padding: 10px 16px; box-shadow: 0 10px 26px rgba(0,0,0,.22);
      backdrop-filter: blur(8px);
    }

    .nav { max-width: 1100px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 12px; }
    .nav ul { list-style: none; padding: 0; margin: 0; display: flex; gap: 4px; align-items: center; }
    .nav li a {
      display: inline-flex; align-items: center; justify-content: center;
      padding: 10px 12px; text-decoration: none; border-radius: 10px;
      transition: background-color .2s ease, transform .2s ease;
    }
    .nav li a:hover { background: rgba(0,0,0,.06); }
    .header.scrolled .nav li a:hover { background: rgba(255,255,255,.12); }
    .nav li a:focus-visible { outline: none; box-shadow: var(--ring); }

    .menu-btn {
      display: none; border: 0; background: transparent; color: inherit;
      padding: 10px 12px; border-radius: 10px; cursor: pointer;
    }
    .menu-btn:focus-visible { outline: none; box-shadow: var(--ring); }

    @media (max-width: 720px) {
      .menu-btn { display: inline-flex; }
      .nav ul {
        position: absolute; left: 12px; right: 12px; top: calc(100% + 10px);
        background: inherit; color: inherit; box-shadow: var(--shadow);
        border-radius: 14px; padding: 8px; flex-direction: column; align-items: stretch;
        display: none; /* toggled via [data-open] */
      }
      .header[data-open="true"] .nav ul { display: flex; }
      .nav li a { justify-content: flex-start; }
    }

    /* Example page spacing (optional) */
    main { max-width: 1100px; margin: 0 auto; padding: 28px 16px; }
  </style>
</head>
<body>
  <header class="header" data-open="false">
    <nav class="nav" aria-label="Primary">
      <button class="menu-btn" type="button" aria-expanded="false" aria-controls="primary-nav">
        Menu
      </button>
      <ul id="primary-nav">
        <li><a href="#">Home</a></li>
        <li><a href="#">About</a></li>
        <li><a href="#">Services</a></li>
        <li><a href="#">Contact</a></li>
      </ul>
    </nav>
  </header>

  <main>
    <h1>Scroll to see the header adjust</h1>
    <p>This is example content to demonstrate sticky behavior.</p>
  </main>

  <script>
    // Efficient scroll handling: use rAF to avoid layout thrashing on rapid scroll events.
    (() => {
      const header = document.querySelector(".header");
      const menuBtn = document.querySelector(".menu-btn");
      const navList = document.getElementById("primary-nav");
      if (!header || !menuBtn || !navList) return;

      const SCROLL_THRESHOLD = 50;
      let ticking = false;

      const updateHeaderOnScroll = () => {
        const isScrolled = window.scrollY > SCROLL_THRESHOLD;
        header.classList.toggle("scrolled", isScrolled);
        ticking = false;
      };

      const onScroll = () => {
        if (!ticking) {
          ticking = true;
          requestAnimationFrame(updateHeaderOnScroll);
        }
      };

      const setMenuOpen = (open) => {
        header.dataset.open = String(open);
        menuBtn.setAttribute("aria-expanded", String(open));
      };

      // Init + listeners
      updateHeaderOnScroll();
      window.addEventListener("scroll", onScroll, { passive: true });

      menuBtn.addEventListener("click", () => setMenuOpen(header.dataset.open !== "true"));

      // Close on outside click (mobile) to prevent stuck-open menus.
      document.addEventListener("click", (e) => {
        if (header.dataset.open !== "true") return;
        if (!header.contains(e.target)) setMenuOpen(false);
      });

      // Close on Escape for accessibility.
      document.addEventListener("keydown",
Back to Snippets