Glassmorphism Card CSS with Frosted Navbar, Login Panel and Stat Chips over a Photo

Four pieces of glassmorphism that share one CSS recipe: a frosted glass card for a listing, a pill navbar, a darker glass login panel and small stat chips, all sitting on a real sunset photograph. A segmented control switches the frost between light, medium and heavy so you can see how blur and tint change readability, and a Preview fallback switch shows exactly what visitors get in a browser without backdrop-filter. On phones the navbar hides its links, and the card, chips and login panel stack in one column.

Code

import { useState } from "react";
import { motion, useReducedMotion } from "framer-motion";

// The photo behind the glass. Glass only reads as glass over something with colour and detail.
const PHOTO = "https://images.pexels.com/photos/11143817/pexels-photo-11143817.jpeg?auto=compress&cs=tinysrgb&w=1600";

// Three strengths of frost. Each sets the blur, the tint and the edge through CSS variables.
const FROSTS = {
  light: { label: "Light", blur: "10px", tint: "0.10", edge: "0.28" },
  medium: { label: "Medium", blur: "18px", tint: "0.16", edge: "0.34" },
  heavy: { label: "Heavy", blur: "30px", tint: "0.24", edge: "0.42" },
} as const;
type Frost = keyof typeof FROSTS;

const links = ["Trips", "Cabins", "Guides", "Journal"];
const stats = [
  { label: "Water", value: "11°C" },
  { label: "Wind", value: "6 km/h" },
  { label: "Sunset", value: "21:48" },
];

// The glass recipe. Copy this block into your own stylesheet.
const CSS = `
.s2g-glass {
  background: rgba(255, 255, 255, var(--s2g-tint, 0.16));
  -webkit-backdrop-filter: blur(var(--s2g-blur, 18px)) saturate(160%);
  backdrop-filter: blur(var(--s2g-blur, 18px)) saturate(160%);
  border: 1px solid rgba(255, 255, 255, var(--s2g-edge, 0.34));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.35), 0 12px 40px -12px rgba(15, 10, 5, 0.45);
}
.s2g-glass-dark {
  background: rgba(20, 16, 14, calc(var(--s2g-tint, 0.16) + 0.22));
  -webkit-backdrop-filter: blur(var(--s2g-blur, 18px)) saturate(140%);
  backdrop-filter: blur(var(--s2g-blur, 18px)) saturate(140%);
  border: 1px solid rgba(255, 255, 255, 0.14);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12), 0 12px 40px -12px rgba(0, 0, 0, 0.5);
}
/* No backdrop-filter support (or the preview switch is on): solid panels that stay readable. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .s2g-glass { background: rgba(58, 44, 38, 0.92); }
  .s2g-glass-dark { background: rgba(24, 20, 18, 0.94); }
}
.s2g-nofilter .s2g-glass { background: rgba(58, 44, 38, 0.92); -webkit-backdrop-filter: none; backdrop-filter: none; }
.s2g-nofilter .s2g-glass-dark { background: rgba(24, 20, 18, 0.94); -webkit-backdrop-filter: none; backdrop-filter: none; }
`;

const ring = "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-stone-900";

function GlassNavbar() {
  const [active, setActive] = useState("Trips");
  return (
    <nav aria-label="Main" className="s2g-glass flex items-center justify-between gap-3 rounded-full py-2 pl-5 pr-2 text-white">
      <a href="#" className={`rounded-full text-[15px] font-semibold tracking-tight ${ring}`}>Fjordline</a>
      <ul className="hidden items-center gap-1 sm:flex">
        {links.map((l) => (
          <li key={l}>
            <a
              href="#"
              aria-current={active === l ? "page" : undefined}
              onClick={(e) => { e.preventDefault(); setActive(l); }}
              className={`inline-flex h-10 items-center rounded-full px-4 text-sm transition-colors ${active === l ? "bg-white/25 font-medium" : "text-white/85 hover:bg-white/10"} ${ring}`}
            >
              {l}
            </a>
          </li>
        ))}
      </ul>
      <a href="#" className={`inline-flex h-10 items-center rounded-full bg-white px-4 text-sm font-semibold text-stone-900 hover:bg-white/90 ${ring}`}>Book a cabin</a>
    </nav>
  );
}

function GlassCard() {
  return (
    <article className="s2g-glass rounded-3xl p-6 text-white">
      <p className="text-xs font-medium uppercase tracking-[0.14em] text-white/80">Frosted card</p>
      <h2 className="mt-3 text-2xl font-semibold tracking-tight">Lakeshore cabin, three nights</h2>
      <p className="mt-2 text-sm leading-relaxed text-white/85">A timber cabin on the north shore with a wood stove, a rowing boat and the ridge line out of every window.</p>
      <div className="mt-6 flex items-end justify-between gap-4">
        <p><span className="text-3xl font-semibold tracking-tight">€420</span><span className="ml-1 text-sm text-white/80">total</span></p>
        <a href="#" className={`inline-flex h-11 items-center rounded-xl border border-white/40 px-4 text-sm font-medium hover:bg-white/15 ${ring}`}>See dates</a>
      </div>
    </article>
  );
}

function GlassLogin() {
  return (
    <form className="s2g-glass-dark rounded-3xl p-6 text-white" onSubmit={(e) => e.preventDefault()} aria-labelledby="s2g-login-title">
      <h2 id="s2g-login-title" className="text-xl font-semibold tracking-tight">Sign in to your trips</h2>
      <p className="mt-1 text-sm text-white/75">Glass login panel, darker tint for form contrast.</p>
      <label htmlFor="s2g-email" className="mt-5 block text-sm font-medium">Email</label>
      <input id="s2g-email" type="email" autoComplete="email" placeholder="[email protected]"
        className="mt-1.5 h-11 w-full rounded-xl border border-white/20 bg-white/10 px-3 text-sm text-white placeholder:text-white/55 focus:border-white/60 focus:outline-none focus:ring-2 focus:ring-white/40" />
      <label htmlFor="s2g-pass" className="mt-4 block text-sm font-medium">Password</label>
      <input id="s2g-pass" type="password" autoComplete="current-password"
        className="mt-1.5 h-11 w-full rounded-xl border border-white/20 bg-white/10 px-3 text-sm text-white focus:border-white/60 focus:outline-none focus:ring-2 focus:ring-white/40" />
      <button type="submit" className={`mt-5 h-11 w-full rounded-xl bg-white text-sm font-semibold text-stone-900 hover:bg-white/90 ${ring}`}>Sign in</button>
    </form>
  );
}

function StatChips() {
  return (
    <ul className="flex flex-wrap gap-2" aria-label="Conditions tonight">
      {stats.map((s) => (
        <li key={s.label} className="s2g-glass inline-flex h-10 items-center gap-2 rounded-full px-4 text-sm text-white">
          <span className="text-white/80">{s.label}</span>
          <span className="font-semibold tabular-nums">{s.value}</span>
        </li>
      ))}
    </ul>
  );
}

export default function GlassmorphismSet() {
  const reduce = useReducedMotion();
  const [frost, setFrost] = useState<Frost>("medium");
  const [noFilter, setNoFilter] = useState(false);
  const f = FROSTS[frost];
  const enter = (i: number) => ({
    initial: reduce ? false : { opacity: 0, y: 14 },
    animate: { opacity: 1, y: 0 },
    transition: { type: "spring", stiffness: 260, damping: 28, delay: reduce ? 0 : 0.08 * i },
  } as const);

  return (
    <section
      className={`relative min-h-[680px] overflow-hidden bg-stone-800 font-[Inter_Tight,Inter,ui-sans-serif,system-ui] ${noFilter ? "s2g-nofilter" : ""}`}
      style={{ ["--s2g-blur" as string]: f.blur, ["--s2g-tint" as string]: f.tint, ["--s2g-edge" as string]: f.edge }}
    >
      <style>{CSS}</style>
      <img src={PHOTO} alt="Mountain lake at sunset with a dark rock in the still water" width={1600} height={1066}
        className="absolute inset-0 h-full w-full object-cover motion-safe:animate-[s2g-drift_24s_ease-in-out_infinite_alternate]" />
      <style>{`@keyframes s2g-drift { from { transform: scale(1.04) translateX(0); } to { transform: scale(1.08) translateX(-1.5%); } }`}</style>
      <div className="absolute inset-0 bg-gradient-to-b from-stone-900/45 via-stone-900/5 to-stone-900/40" aria-hidden="true" />

      <div className="relative mx-auto flex min-h-[680px] max-w-5xl flex-col justify-between gap-6 px-4 py-6 sm:px-6 sm:py-10">
        <motion.div {...enter(0)}><GlassNavbar /></motion.div>

        <div className="grid gap-6 md:grid-cols-[1.15fr_1fr] md:items-end md:pb-6">
          <div className="flex flex-col gap-5">
            <motion.div {...enter(1)}><GlassCard /></motion.div>
            <motion.div {...enter(2)}><StatChips /></motion.div>
            <motion.div {...enter(3)} className="s2g-glass-dark flex flex-wrap items-center gap-3 rounded-2xl p-3 text-white">
              <div role="radiogroup" aria-label="Frost strength" className="flex rounded-full bg-white/10 p-1">
                {(Object.keys(FROSTS) as Frost[]).map((k) => (
                  <button key={k} type="button" role="radio" aria-checked={frost === k} onClick={() => setFrost(k)}
                    className={`h-9 rounded-full px-3.5 text-sm transition-colors ${frost === k ? "bg-white font-semibold text-stone-900" : "text-white/85 hover:bg-white/10"} ${ring}`}>
                    {FROSTS[k].label}
                  </button>
                ))}
              </div>
              <button type="button" role="switch" aria-checked={noFilter} onClick={() => setNoFilter((v) => !v)}
                className={`inline-flex h-10 items-center gap-2 rounded-full px-3 text-sm text-white/90 hover:bg-white/10 ${ring}`}>
                <span aria-hidden="true" className={`relative inline-block h-5 w-9 shrink-0 rounded-full transition-colors ${noFilter ? "bg-white" : "bg-white/25"}`}>
                  <span className={`absolute left-0 top-0.5 h-4 w-4 rounded-full transition-transform ${noFilter ? "translate-x-[18px] bg-stone-900" : "translate-x-0.5 bg-white/90"}`} />
                </span>
                Preview fallback
              </button>
            </motion.div>
          </div>
          <motion.div {...enter(2)}><GlassLogin /></motion.div>
        </div>
      </div>
    </section>
  );
}

Install with one command: npx shadcn@latest add https://we.inc/r/glassmorphism-card-css-with-frosted-navbar-login-panel-and-stat-chips-over-a-photo.json

When to use it

How to customize it

Accessibility

Questions

How does the glassmorphism CSS work?

Three properties do the work. `backdrop-filter: blur()` blurs whatever is behind the element, a semi-transparent background such as `rgba(255,255,255,0.16)` gives it a milky tint, and a thin light border plus an inset highlight on the top edge makes it read as a pane with thickness. `saturate(160%)` keeps the blurred colours from going grey.

Why does my glass card look like a plain grey box?

Backdrop blur only shows when there is something behind the element to blur. On a flat white or single-colour background the result is just a translucent rectangle. Put glass over a photograph, a gradient or shapes, and make sure no parent between the card and that background has its own opaque fill.

What happens in browsers without backdrop-filter?

The `@supports not` block detects the missing feature and gives each panel an almost opaque solid colour instead, so text never ends up floating over a raw photo. The Preview fallback switch applies the same rules on demand so you can check the result.

Do I need the -webkit- prefix?

Safari supported only the prefixed `-webkit-backdrop-filter` for years, and older iPhones still in use need it. Keeping both lines costs nothing, so the recipe writes the prefixed and standard property side by side.

Is backdrop-filter slow?

Blurring is done by the graphics card every time something behind the panel moves, so large blur radii over animated content can drop frames on weak phones. Keep the radius at 30px or less, avoid stacking many glass layers on top of each other, and do not animate the blur value itself.

Can I use Tailwind classes instead of custom CSS?

Yes. `bg-white/15 backdrop-blur-lg backdrop-saturate-150 border border-white/30` gets close, and `supports-[not(backdrop-filter:blur(1px))]:bg-stone-800/90` adds the fallback. The CSS variables here are used so one picker can change every panel at once.

Related: Mountain Lodge Photo Hero with Drifting Cloud Headline and Glass Booking Cards, Black Glass Pill Navbar with a Mercury Drop That Stretches to the Active Link, Gift Card with a Silver Scratch off Panel, a Brass Coin and Foil Crumbs That Scatter as You Rub.

More cards or all free UI components. Start for free to have a full site built around it.

More in Cards

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap