Skip to content
Plinth

Original by Plinth ยท MIT

Navigation

Glass Nav Bar

A floating Liquid Glass tab bar whose bright lens springs across to the tab you pick.

#liquid-glass#navigation#tab-bar#backdrop-filter#javascript

Install

npx shadcn@latest add https://plinthui.com/r/glass-nav-bar.json
Open in v0
'use client';

import { useState } from 'react';

const ITEM_W = 72;
const PAD = 6;


// Glass only reads against something behind it โ€” place this over an image,
// a gradient or busy content; on a plain white background it will look
// nearly invisible. The material (backdrop-filter, a mask-composite rim,
// the pointer-driven highlight) is outside what Tailwind can express, so it
// lives in the embedded style element below.
const css = `
.glass-nav {
  position: relative;
  isolation: isolate;
  box-sizing: border-box;
  display: flex;
  padding: ${PAD}px;
  border-radius: 999px;
  background:
    linear-gradient(150deg, rgba(255, 255, 255, 0.3) 0%, rgba(255, 255, 255, 0.09) 48%, rgba(255, 255, 255, 0.19) 100%);
  -webkit-backdrop-filter: blur(18px) saturate(200%) brightness(1.14);
  backdrop-filter: blur(18px) saturate(200%) brightness(1.14);
  box-shadow:
    inset 0 1px 1px rgba(255, 255, 255, 0.66),
    inset 0 -1px 1px rgba(255, 255, 255, 0.26),
    inset 0 0 20px -6px rgba(255, 255, 255, 0.42),
    0 2px 6px rgba(8, 10, 40, 0.2),
    0 14px 28px -10px rgba(8, 10, 40, 0.38),
    0 32px 56px -26px rgba(8, 10, 40, 0.5);
}
.glass-nav::before {
  content: "";
  position: absolute;
  inset: 0;
  padding: 1.5px;
  border-radius: inherit;
  pointer-events: none;
  z-index: 3;
  background: linear-gradient(
    150deg,
    rgba(255, 255, 255, 1) 0%,
    rgba(255, 255, 255, 0.5) 18%,
    rgba(255, 255, 255, 0.06) 44%,
    rgba(255, 255, 255, 0.06) 62%,
    rgba(255, 255, 255, 0.8) 100%
  );
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
}
.glass-nav::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: 2;
  background: linear-gradient(
    104deg,
    transparent 34%,
    rgba(255, 255, 255, 0.3) 48%,
    rgba(255, 255, 255, 0.06) 56%,
    transparent 66%
  );
  background-size: 260% 100%;
  animation: glass-nav-sweep 7s cubic-bezier(0.45, 0, 0.2, 1) infinite;
}
@keyframes glass-nav-sweep {
  0%        { background-position: 150% 0; }
  55%, 100% { background-position: -60% 0; }
}
.glass-nav__lens {
  position: absolute;
  top: ${PAD}px;
  height: calc(100% - ${PAD * 2}px);
  border-radius: 999px;
  pointer-events: none;
  background: linear-gradient(160deg, rgba(255, 255, 255, 0.62), rgba(255, 255, 255, 0.26));
  box-shadow:
    inset 0 1px 1px rgba(255, 255, 255, 0.95),
    inset 0 -1px 2px rgba(255, 255, 255, 0.5),
    0 3px 8px rgba(8, 10, 40, 0.22);
  transition:
    left 480ms cubic-bezier(0.34, 1.42, 0.5, 1),
    width 480ms cubic-bezier(0.34, 1.42, 0.5, 1);
}
.glass-nav__item {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  width: ${ITEM_W}px;
  padding: 8px 0 9px;
  border: 0;
  border-radius: 999px;
  background: none;
  font: inherit;
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: rgba(255, 255, 255, 0.86);
  text-shadow: 0 1px 2px rgba(8, 10, 40, 0.35);
  cursor: pointer;
  transition: color 320ms ease, text-shadow 320ms ease;
  -webkit-tap-highlight-color: transparent;
}
.glass-nav__item svg { width: 20px; height: 20px; display: block; }
.glass-nav__item[aria-current="page"] {
  color: #17124a;
  text-shadow: none;
}
.glass-nav__item:focus-visible {
  outline: 2px solid rgba(255, 255, 255, 0.9);
  outline-offset: -2px;
}
`;

const LABELS = ['Home', 'Search', 'Library', 'Profile'];

function NavIcon({ index }: { index: number }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.7"
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      {index === 0 ? (
        <g>
          <path d="M3.2 10.6 12 3.4l8.8 7.2" />
          <path d="M5.6 9.6V20h12.8V9.6" />
        </g>
      ) : null}
      {index === 1 ? (
        <g>
          <circle cx="11" cy="11" r="6.4" />
          <path d="M15.8 15.8 20.6 20.6" />
        </g>
      ) : null}
      {index === 2 ? (
        <g>
          <path d="M12 3.4 20.6 8 12 12.6 3.4 8Z" />
          <path d="M3.4 13.2 12 17.8l8.6-4.6" />
        </g>
      ) : null}
      {index === 3 ? (
        <g>
          <circle cx="12" cy="8.6" r="3.8" />
          <path d="M4.9 20a7.3 7.3 0 0 1 14.2 0" />
        </g>
      ) : null}
    </svg>
  );
}

export function GlassNavBar({ initialTab = 0 }: { initialTab?: number }) {
  const [active, setActive] = useState(initialTab);

  return (
    <>
      <style>{css}</style>

      <nav className="glass-nav" aria-label="Primary">
        {/* Every tab is the same fixed width, so the lens needs no measuring. */}
        <span
          className="glass-nav__lens"
          aria-hidden="true"
          style={{ left: PAD + active * ITEM_W, width: ITEM_W }}
        />
        {LABELS.map((label, i) => (
          <button
            key={label}
            type="button"
            className="glass-nav__item"
            aria-current={i === active ? 'page' : undefined}
            onClick={() => setActive(i)}
          >
            <NavIcon index={i} />
            {label}
          </button>
        ))}
      </nav>
    </>
  );
}

More in Navigation

Similar tags