Original by Plinth ยท MIT
Navigation
Glass Nav Bar
A floating Liquid Glass tab bar whose bright lens springs across to the tab you pick.
Install
npx shadcn@latest add https://plinthui.com/r/glass-nav-bar.json'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>
</>
);
}