EmpireUI
Get Pro
← Blog7 min read#glassmorphism#navigation#react

Frosted Glass Navigation: Production-Ready Glassmorphism Header

Build a production-ready frosted glass navigation bar with React and Tailwind v4. Scroll-aware blur, dark mode, and zero layout shift — fully open source.

Frosted glass UI navigation bar floating over a vibrant purple-blue gradient background

Why Frosted Glass Navigation Is Everywhere Right Now

Honestly, the frosted glass nav is the single most-copied UI pattern of the last three years — and most implementations are broken. They look right in a Figma mockup, then jitter on scroll, paint the wrong color in dark mode, or crash Safari with a backdrop-filter compositing bug. This guide fixes all of that.

The appeal is obvious. A sticky header that lets the page content bleed through it feels lighter and less intrusive than a solid bar. It signals depth without requiring actual shadow math. Apple's done it in every OS since Big Sur, Vercel uses it, Linear uses it, and now every SaaS landing page wants it. The problem is the details.

If you haven't read what glassmorphism actually is yet, skim that first. This article jumps straight into production code. We're assuming you know React 18+, Tailwind v4.0.2 or later, and you've already decided glassmorphism is the right aesthetic choice for your project.

The CSS Foundation: What Actually Creates the Frosted Effect

Three properties do all the work. backdrop-filter: blur(12px) blurs whatever is rendered behind the element — not inside it. background: rgba(255,255,255,0.08) sets a near-transparent white wash over that blur so text remains legible. border-bottom: 1px solid rgba(255,255,255,0.12) draws a glass edge that separates the nav from the page visually.

The blur radius matters more than you'd think. At 4px it looks like a dirty window. At 24px it starts bleeding too much of the background color into the nav text. The sweet spot for a sticky header is 10px14px. We'll default to 12px in the component.

One thing people always forget: backdrop-filter only works when the element has a non-fully-transparent background. Set background: transparent and the blur disappears silently — no error, just nothing. Also, you need will-change: transform or a GPU-composited layer to avoid repainting the blur on every scroll frame. Missing that one single property causes the jitter most people blame on React.

Building the Base Glassmorphism Nav Component in React

Here's the full component. It's scroll-aware — the blur and border only activate once the user has scrolled past the hero, keeping the header invisible-but-present at the top of the page.

'use client';

import { useEffect, useState } from 'react';

interface GlassNavProps {
  logo: React.ReactNode;
  links: { label: string; href: string }[];
  cta?: React.ReactNode;
}

export function GlassNav({ logo, links, cta }: GlassNavProps) {
  const [scrolled, setScrolled] = useState(false);

  useEffect(() => {
    const onScroll = () => setScrolled(window.scrollY > 60);
    window.addEventListener('scroll', onScroll, { passive: true });
    return () => window.removeEventListener('scroll', onScroll);
  }, []);

  return (
    <header
      className={[
        'fixed top-0 left-0 right-0 z-50',
        'transition-all duration-300 ease-in-out',
        'will-change-transform',
        scrolled
          ? 'backdrop-blur-[12px] bg-white/8 border-b border-white/12 shadow-sm'
          : 'bg-transparent border-b border-transparent',
      ].join(' ')}
    >
      <nav className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
        <div className="text-white font-semibold text-lg">{logo}</div>
        <ul className="hidden md:flex gap-8">
          {links.map((l) => (
            <li key={l.href}>
              <a
                href={l.href}
                className="text-white/80 hover:text-white text-sm font-medium transition-colors"
              >
                {l.label}
              </a>
            </li>
          ))}
        </ul>
        {cta && <div>{cta}</div>}
      </nav>
    </header>
  );
}

A few things worth calling out: { passive: true } on the scroll listener tells the browser you'll never call preventDefault(), which lets it skip the main thread check and keeps scroll buttery. The will-change-transform class forces a GPU compositing layer so the blur doesn't repaint on every frame. And the conditional class string is deliberately flat — no clsx required, just a join.

Tailwind v4 Arbitrary Values and the Opacity Problem

Tailwind v4.0.2 ships backdrop-blur-* utilities out of the box, but bg-white/8 uses the opacity modifier syntax that maps to rgb(255 255 255 / 0.08) in oklch color space. That's subtly different from the old rgba(255,255,255,0.08) — you'll get slightly warmer whites on displays with wide-gamut support. Not a bug, just something to know.

If you need pixel-perfect control over the color, use an arbitrary value: bg-[rgba(255,255,255,0.08)]. Ugly to type but exact. In v4 you can also define a CSS custom property in your @theme block and reference it with the theme() function, which is cleaner for design systems that have a single glass surface color.

One gotcha with backdrop-blur-[12px]: Tailwind's JIT scanner needs to see the full string in source — don't build it dynamically with template literals or the class won't be generated. If you're computing blur based on route, define all variants as static strings and use conditional rendering instead.

Dark Mode: Making the Glass Nav Actually Work on Dark Backgrounds

This is where most tutorials fall apart. On a dark page — say, bg-gray-950 or a deep navy gradient — bg-white/8 looks fine. But flip to a light background and that same rgba(255,255,255,0.08) is invisible; you can't tell where the nav ends and the page begins. You need to adapt the background tint to the page theme.

The cleanest approach is a CSS custom property toggled by your dark mode class. Something like --glass-bg: rgba(255,255,255,0.08) in .dark and rgba(0,0,0,0.06) in :root. Then bg-[var(--glass-bg)] in Tailwind. This plays well with theme toggle implementations in React that flip a class on <html>.

Don't use dark:bg-black/6 as the only strategy. The Tailwind dark variant relies on the system preference *or* a class toggle, depending on your config. If your toggle logic uses a JS-managed class on <html> but your Tailwind config still has darkMode: 'media', the dark variant will be ignored. Double-check your tailwind.config.ts before blaming the component.

Scroll-Aware Blur vs. Always-On: When to Use Each

The scroll-activated pattern we built above — transparent at top, frosted after 60px — works best when your hero fills the viewport. It lets the nav float invisibly over a full-bleed image or gradient without a colored box interrupting the design. The transition into frosted glass also signals context to the user: "you've left the hero, now you're in the content."

Always-on blur makes more sense for app shells — dashboards, editors, admin panels — where there's no hero and the nav is functional rather than decorative. In those cases you don't want the nav changing appearance mid-session. Just set scrolled to true permanently or skip the state entirely. Simpler is better.

Is there a performance cost to always-on backdrop-filter? Yes, but it's small on modern hardware. The blur is GPU-accelerated once you have a compositing layer. Where you'll actually see frame drops is when you blur *too many* stacked elements simultaneously — a nav, a modal, and a sidebar all blurring at once will hurt on mid-range Android. Pick your battles.

Browser Support, Safari Caveats, and the backdrop-filter Fallback

As of late 2026, backdrop-filter has 97%+ global support. The only meaningful holdout is very old Android WebView. Safari has supported it since version 9 — but it requires the -webkit- prefix if you're targeting Safari 14 or earlier. If you're using Tailwind, the backdrop-blur-* utilities already emit both the prefixed and unprefixed versions, so you don't need to think about it.

There's one real Safari bug worth knowing: if an ancestor of your blurred element has overflow: hidden or transform: translateZ(0) applied, the backdrop-filter can stop working entirely and render as fully transparent. This trips people up when they wrap the entire app in an animation container. Check with DevTools if your blur disappears on Safari — look for overflow: hidden climbing the DOM tree.

The graceful fallback for no-support browsers is easy: just make the background slightly more opaque. bg-white/20 without backdrop-blur still reads as a distinct nav panel, it just won't frost the content behind it. Add @supports not (backdrop-filter: blur(1px)) in your CSS if you want to target that fallback precisely. Empire UI's glassmorphism component collection handles this automatically.

Adding a Mobile Menu That Doesn't Break the Glass Effect

The nav we built hides links on mobile with hidden md:flex. You'll want a hamburger that opens a drawer or dropdown. The trap here is making that dropdown *also* glassy — because now you have a blurred nav at the top and a blurred menu overlapping the page content, and compositing two backdrop-filter layers on top of each other causes visual artifacts on some browsers.

The safer approach: give the mobile drawer a solid-but-translucent background (bg-gray-900/95 in dark mode, bg-white/95 in light) rather than a full glass blur. It still reads as airy and modern, avoids the double-blur compositing issue, and performs better on phones. Reserve the glass effect for the header strip itself.

Also set overscroll-behavior: contain on the drawer and lock body scroll when it's open. If the page scrolls while the drawer is open, your scroll-aware nav state will fire incorrectly and you'll see the blur toggle mid-animation. It's a subtle bug but users notice it. The fix is two lines: document.body.style.overflow = 'hidden' on open, document.body.style.overflow = '' on close. For more complex navigation layouts, compare approaches in Tailwind vs CSS Modules — the tradeoffs around utility classes show up fast in stateful components like this.

FAQ

Why does my `backdrop-filter: blur()` not work in Safari even though I added the prefix?

Check for an ancestor element with overflow: hidden or a 3D transform like translateZ(0) or translate3d(). Safari doesn't paint backdrop-filter through those compositing boundaries. Remove the overflow or move the blurred element outside the transformed ancestor.

What's the right blur radius for a sticky navigation bar?

10px to 14px is the practical range for a nav. Below 8px the frost effect reads as a dirty window rather than glass. Above 16px the background colors bleed into text and reduce legibility. 12px is a safe default — adjust up or down depending on how saturated your background gradient is.

Will `backdrop-filter` hurt scroll performance?

Not significantly on desktop or modern phones, provided the element has a GPU compositing layer. Add will-change: transform or transform: translateZ(0) to the nav element. Without a compositing layer the browser repaints the blur on every scroll frame, which causes the jitter you're probably seeing.

How do I make the frosted nav work with Tailwind dark mode?

Set darkMode: 'class' in tailwind.config.ts, then use dark:bg-black/6 alongside your default bg-white/8. If you're toggling dark mode via JS, make sure you're flipping the class on the <html> element — not <body>. Tailwind's dark variant only watches the html element by default.

Can I use this glassmorphism nav with Next.js App Router?

Yes — add the 'use client' directive at the top (it's already in the code example above) because the component uses useState and useEffect. Export it from a components/ directory and import it in your root layout.tsx. Don't try to make it a Server Component; the scroll listener requires the client.

How do I prevent layout shift when the nav becomes sticky on scroll?

Give the <header> position: fixed from the start rather than switching from static to fixed on scroll. Fixed positioning removes the element from flow immediately, so there's nothing to shift. Add padding-top equal to the nav height on the first content section to compensate for the removed flow space.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

The Ultimate CSS UI Styles Guide: All 40 Visual Styles Ranked (2026)Glassmorphism Sidebar Navigation in React: Frosted Side PanelPage Header Variants: 8 Designs for App and Marketing PagesSEO Breadcrumbs in React: Schema Markup and Accessible Navigation