Next.js Sticky Header That Changes on Scroll (Blur Header Bar)

Published: September 8, 2026 · 7 min read

Build a Next.js fixed header that turns from transparent to blurred on scroll: sticky vs fixed, a 16px threshold, a Liquid Glass look, and hide-on-scroll.

Next.js fixed header: transparent first, background on scroll

You built a landing page in Next.js and put a photo or animation in the hero. But the solid white header bar always cuts across the top of the hero, and the page ends up looking like an old corporate site.

The quickest fix is to let the header change with scroll position.

Here is the answer. When the page loads, keep the header background transparent so the hero shows through.

After about 16px of scroll, add a semi-transparent background and a backdrop blur. Only the header file reads the scroll, so the rest of the page can stay Server Components.

This guide covers:

  • Choosing between sticky and fixed for the header base

  • Code for a header bar whose background and blur change on scroll

  • Finishing it as a Liquid Glass-style floating island

  • A header that hides on scroll down and returns on scroll up

Why a fixed header that changes on scroll suits landing pages

If an opaque bar is there from the first frame, it always hides the top of the hero. On a landing page built around a photo or motion, the header eats into the first impression.

Leave it transparent forever, though, and once the reader reaches the body, the link text overlaps photos and paragraphs and becomes unreadable. Transparent first, translucent after scrolling solves both problems at once.

The change can be small. A background tint, a blur, and a thin border are enough to make the navigation read as a layer floating above the page. Changing the logo size or shape on top of that usually makes it feel restless.

Fixed header base: sticky vs fixed

A header that stays at the top can be built with position: sticky or position: fixed. They look similar, but they overlap the hero differently.

  • sticky: the header takes up its own height in the layout. The hero starts below it, so even a transparent header does not sit on top of the hero

  • fixed: the header takes no space and floats above the page. A transparent header can sit on the hero, but the first content on other pages needs top padding so it is not hidden

  • If you want sticky and overlap: pull the hero up by the header height with a negative margin such as -mt-16

If you want a transparent header over the hero photo, fixed is the straightforward choice. This guide uses fixed and assumes non-hero pages give their first section something like pt-20. For calmer pages where the header and hero do not overlap, sticky is fine.

Step 1: Place a transparent fixed header

First, place a transparent header pinned to the top with no scroll logic. It can stay a Server Component at this point.

Three things matter. Pin it with fixed inset-x-0 top-0, and do not add bg-white from the start (that brings back the bar).

Then use z-50 so it sits above the hero.

components/SiteHeader.tsx
export function SiteHeader() {
    return (
      <header className="fixed inset-x-0 top-0 z-50 px-6 py-4">
        <div className="mx-auto flex max-w-6xl items-center justify-between">
          <a href="/" className="text-sm font-semibold tracking-wide">
            NORTHLINE
          </a>
          <nav className="hidden gap-8 text-sm md:flex">
            <a href="#work">Work</a>
            <a href="#about">About</a>
            <a href="#contact">Contact</a>
          </nav>
        </div>
      </header>
    );
  }

What a transparent fixed header alone lacks

The background stays transparent while you scroll. Once you reach the body, links overlap the content underneath. Step 2 adds a background based on scroll position.

Step 2: Switch the header bar background and blur on scroll

The core idea is one true/false value. Store whether the scroll position (window.scrollY) has passed a threshold, and swap between the transparent classes and the translucent ones.

components/ScrollHeader.tsx
"use client";

  import { useEffect, useState } from "react";

  export function ScrollHeader() {
    const [scrolled, setScrolled] = useState(false);

    useEffect(() => {
      const onScroll = () => setScrolled(window.scrollY > 16);
      onScroll(); // Also correct after reloading mid-page
      window.addEventListener("scroll", onScroll, { passive: true });
      return () => window.removeEventListener("scroll", onScroll);
    }, []);

    return (
      <header
        className={
          "fixed inset-x-0 top-0 z-50 transition-colors duration-300 " +
          (scrolled
            ? "border-b border-zinc-200/60 bg-white/75 backdrop-blur-md"
            : "border-b border-transparent bg-transparent")
        }
      >
        <div className="mx-auto flex h-16 max-w-6xl items-center justify-between px-6">
          <a href="/" className="text-sm font-semibold">
            NORTHLINE
          </a>
          <nav className="hidden gap-8 text-sm md:flex">
            <a href="#work">Work</a>
            <a href="#about">About</a>
            <a href="#contact">Contact</a>
          </nav>
        </div>
      </header>
    );
  }

Run only the fixed header file in the browser

Reading window requires "use client" (the marker that says this component runs in the browser), but only in this file.

The shared layout.tsx stays rendered on the server and simply renders <ScrollHeader />. Moving the whole layout to the browser adds JavaScript to every page.

Start with a 16px threshold

Switching at 1 to 2px makes the header flicker on tiny trackpad movements. Start at 16px, raise it if it flickers, lower it if the switch feels late.

A range of 8 to 24px is a good starting point. You rarely need to wait for the full hero height.

Do not restyle the header directly on every scroll

Writing element.style.backgroundColor on every scroll event adds work and tends to stutter. Remembering only "past the threshold or not" means React re-renders just twice, at the moments it switches. Add { passive: true } to the scroll listener and always remove it when the component unmounts.

What to change in the header and what to keep fixed

Safe things to switch: background (transparent vs translucent), whether backdrop-blur is on, the border, and text color if needed. Keep the logo shape, size, and nav order fixed. A logo that grows and shrinks on scroll starts to look like an ad.

Step 3: Finish it as a Liquid Glass-style island header

Liquid Glass is Apple's glass-like material for navigation layers in native apps. The web has no direct equivalent, so here we approximate it with a translucent background, backdrop-blur, readable text, and a floating shape with space on both sides. It is a visual approximation, not an official port.

components/IslandHeader.tsx (replaces the header in Step 2)
<header className="fixed inset-x-0 top-0 z-50 px-4 pt-3">
    <div
      className={
        "mx-auto flex h-14 max-w-5xl items-center justify-between rounded-full px-5 transition-all duration-300 " +
        (scrolled
          ? "border border-white/40 bg-white/70 shadow-sm backdrop-blur-md"
          : "border border-transparent")
      }
    >
      {/* Logo and nav, same as Step 2 */}
    </div>
  </header>

Leave space outside to read as an island header

Rounding the corners of a full-width bar does not make it an island. Keep px-4 or more on the outer header and put the background, radius, and blur only on the inner box.

shadow-sm is plenty. Leaning on heavy shadows for a glass feel gets heavy-handed fast.

Start with a medium blur

Try around backdrop-blur-md first. Too strong and the header becomes expensive to render on phones, and the text gets hazy. Check both states for white text on a white hero.

Avoid stacking blurs

If the hero also has large frosted cards or a strong blur, blurring the header heavily too makes the whole screen look foggy. Let one of them lead and keep the header as readable glass. Frosted-glass techniques in general are covered in https://design-layer.com/en/articles/nextjs-glassmorphism-ui-guide.

A header that hides on scroll down and returns on scroll up

On reading-heavy pages, a common pattern hides the header while scrolling down and brings it back as soon as the reader scrolls up. Keep the previous scroll position in a ref and toggle hidden by the direction of the difference.

components/AutoHideHeader.tsx
"use client";

  import { useEffect, useRef, useState } from "react";

  export function AutoHideHeader() {
    const [hidden, setHidden] = useState(false);
    const [scrolled, setScrolled] = useState(false);
    const lastY = useRef(0);

    useEffect(() => {
      lastY.current = window.scrollY;
      const onScroll = () => {
        const y = window.scrollY;
        setScrolled(y > 16);
        const diff = y - lastY.current;
        if (Math.abs(diff) < 8) return; // Ignore tiny jitters
        setHidden(diff > 0 && y > 120); // Hide when moving down past the hero
        lastY.current = y;
      };
      onScroll();
      window.addEventListener("scroll", onScroll, { passive: true });
      return () => window.removeEventListener("scroll", onScroll);
    }, []);

    return (
      <header
        className={
          "fixed inset-x-0 top-0 z-50 transition duration-300 focus-within:translate-y-0 " +
          (hidden ? "-translate-y-full" : "translate-y-0") +
          (scrolled ? " bg-white/75 backdrop-blur-md" : "")
        }
      >
        {/* Logo and nav */}
      </header>
    );
  }

Pick where to use a hide-on-scroll header

focus-within:translate-y-0 keeps the header visible when a keyboard user tabs into a link. On pages with big scroll-driven sections such as a pinned gallery, though, the header sliding in and out competes with that motion. On landing pages, keeping a light header visible is usually the safer choice.

Fixed header checklist before you ship

  • Logo and links readable in both the transparent and translucent states

  • In-page links (#about) do not land with the heading hidden under the header. Add scroll-padding-top on html or scroll-mt-20 on headings

  • Header state is correct after reloading mid-page

  • Focus rings are visible when tabbing through links

  • Blur is not too heavy on phones, and links are easy to tap

  • With fixed, content on non-hero pages does not slide under the header

On mobile, grouping links into a hamburger menu is usually easier than squeezing in the desktop nav. Menu open and close motion is covered in https://design-layer.com/en/articles/nextjs-mobile-nav-animation-guide.

Copy a finished fixed header instead

The DesignLayer catalog filtered to navigation components
The DesignLayer catalog filtered to navigation components

If you have written Steps 1 and 2 but want to compare finished island or glass headers before deciding, you can run them in the DesignLayer catalog.

Watch the scroll behavior in the live preview, then hand the React + Tailwind code to your AI agent with Copy for Cursor. Browsing needs no card, and free source copies are 3 per day (JST). https://design-layer.com/en/components

These fit the top of landing pages, portfolios, and product pages. Multi-level docs navigation or an in-app bar after login is usually faster to build with plain links.

Next.js fixed header: common questions

  • Yes. Add "use client" only to the header file that reads the scroll position. A plain transparent fixed header can stay a Server Component.

Summary: building a fixed header that changes on scroll

  • Start the Next.js fixed header transparent, and add a translucent background and blur after a little scrolling

  • Float the header to overlap the hero, or let it stick in place to sit above it

  • Remember only whether the page has scrolled, and swap Tailwind classes

  • Approximate Liquid Glass with translucency, a background blur, and outer spacing

  • Check that in-page links do not hide headings and that keyboard focus stays visible

Drop the Step 2 code into your layout.tsx and check how it overlaps your hero. Building the hero underneath is covered in https://design-layer.com/en/articles/nextjs-animated-hero-section-guide.

Related guides

Next.js Sticky Header That Changes on Scroll (Blur Header Bar) | DesignLayer