About
A liquid-glass dock with a raised active icon and a smoothly travelling notch. Customize the links, notch depth, frost, refraction, tint and transition speed. Includes keyboard navigation, host-theme colors, responsive sizing and reduced-motion support.
Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Liquid Glass Notched Dock to your project and fits it to your theme.
Code
How Liquid Glass Notched Dock is used: the demo's code. The component's own source comes with a free account.
Usage.tsx
"use client";
import { useEffect, useState, type MouseEvent, type ReactNode } from "react";
import { ArrowUpRight, CalendarDays, CircleHelp, Folder, House, Inbox, LayoutTemplate, Plus, Settings2, UserRound } from "lucide-react";
import "@fontsource-variable/manrope";
import LiquidGlassNotchedDock, { type GlassNavItem } from "@/components/ui/liquid-glass-notched-dock";
const settings = {
useHostTheme: true,
frost: 2,
opacity: 0.08,
saturation: 1.3,
edgeLight: 0.85,
refraction: 28,
glassTint: "#eee9df",
accentColor: "#9af1d6",
transitionSpeed: 1,
notchDepth: 1,
};
const workspaceStyles = `
.gn-workspace {
--gn-demo-bg: var(--color-background, Canvas);
--gn-demo-card: var(--color-card, Canvas);
--gn-demo-ink: var(--color-card-foreground, var(--color-foreground, CanvasText));
box-sizing: border-box;
position: relative;
isolation: isolate;
display: grid;
place-items: center;
inline-size: 100%;
min-inline-size: 0;
max-inline-size: none;
flex: 1 1 100%;
min-block-size: 100svh;
padding: 52px clamp(16px, 4vw, 56px);
color: var(--gn-demo-ink);
background: var(--gn-demo-bg);
font-family: "Manrope Variable", Manrope, system-ui, sans-serif;
}
.gn-workspace[data-scene="sidebar"] { padding-block: 28px 56px; }
.gn-workspace[data-gn-mode="dark"] { color-scheme: dark; }
.gn-workspace[data-gn-mode="light"] { color-scheme: light; }
.gn-workspace-photo {
position: absolute;
z-index: -2;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-position: var(--gn-photo-position, center);
}
.gn-workspace[data-scene="contour"] { --gn-photo-position: center 68%; }
.gn-workspace[data-scene="spotlight"] { --gn-photo-position: center 58%; }
.gn-workspace[data-scene="notched"] { --gn-photo-position: center 47%; }
.gn-workspace[data-scene="sidebar"] { --gn-photo-position: center 58%; }
.gn-workspace::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
background: color-mix(in srgb, var(--gn-demo-bg) 8%, transparent);
pointer-events: none;
}
.gn-workspace[data-gn-mode="dark"]::before { background: color-mix(in srgb, var(--gn-demo-bg) 18%, transparent); }
.gn-workspace-navigation {
inline-size: min(100%, 560px);
min-inline-size: 0;
}
.gn-workspace[data-scene="sidebar"] .gn-workspace-navigation { inline-size: min(100%, 260px); }
.gn-workspace-credit {
position: absolute;
inset-inline-end: 12px;
inset-block-end: 8px;
display: inline-flex;
align-items: center;
gap: 4px;
min-block-size: 28px;
padding-inline: 9px;
border-radius: 6px;
color: var(--gn-demo-ink);
background: color-mix(in srgb, var(--gn-demo-card) 84%, transparent);
font-size: .6875rem;
text-decoration: none;
}
.gn-workspace-credit:hover { text-decoration: underline; }
.gn-workspace-credit:focus-visible { outline: 2px solid var(--color-ring, Highlight); }
.gn-workspace-credit svg { width: 12px; height: 12px; }
`;
type GlassNavGroup = { label: string; items: (GlassNavItem & { children?: GlassNavItem[] })[] };
type NavigationDemoKind = "contour" | "spotlight" | "notched" | "sidebar";
type NavigationDemoBindings = {
activeId: string;
items: GlassNavItem[];
groups: GlassNavGroup[];
onNavigate: (item: GlassNavItem, event: MouseEvent< HTMLAnchorElement>) => void;
};
const workspaceItems: GlassNavItem[] = [
{ id: "home", href: "#home", label: "Home", icon: <House /> },
{ id: "projects", href: "#projects", label: "Projects", icon: <Folder /> },
{ id: "create", href: "#create", label: "Create", icon: <Plus /> },
{ id: "inbox", href: "#inbox", label: "Inbox", icon: <Inbox />, badge: "2 unread messages" },
{ id: "profile", href: "#profile", label: "Profile", icon: <UserRound /> },
];
const workspaceGroups: GlassNavGroup[] = [
{
label: "Workspace",
items: workspaceItems.slice(0, 4).map(item => item.id === "projects" ? {
...item,
children: [
{ id: "studio", href: "#studio", label: "Studio website", icon: <LayoutTemplate /> },
{ id: "portal", href: "#portal", label: "Client portal", icon: <Folder /> },
],
} : item),
},
{
label: "More",
items: [
{ id: "calendar", href: "#calendar", label: "Calendar", icon: <CalendarDays /> },
{ id: "settings", href: "#settings", label: "Settings", icon: <Settings2 /> },
{ id: "help", href: "#help", label: "Help", icon: <CircleHelp /> },
],
},
];
const initialSelection: Record< NavigationDemoKind, string> = {
contour: "projects", spotlight: "inbox", notched: "create", sidebar: "studio",
};
const dockIds = new Set(workspaceItems.map(item => item.id));
const sidebarIds = new Set([
"profile",
...workspaceGroups.flatMap(group => group.items.flatMap(item => [
item.id,
...(item.children ?? []).map(child => child.id),
])),
]);
// Demo-only scenes. Unsplash credits stay with their source images.
// Generated scenes are project assets, uploaded separately when a demo is published.
const scenes: Record< NavigationDemoKind, { image: string; author: string; source?: string }> = {
contour: {
image: "https://cdn.21st.dev/assets/mirror/78/783f8881b23c99dc41d7682328c17d453aa4303c754b6322aad7ebd815836974.jpg",
author: "Chris Weiher", source: "https://unsplash.com/photos/3epCdVoYCds",
},
spotlight: {
image: "https://cdn.21st.dev/assets/mirror/73/73070a7f6ab0dc22f37769fe232cf0d1ce106dd142d5774ef5a9a215aa240329.png",
author: "Automate Flows",
},
notched: {
image: "https://cdn.21st.dev/assets/mirror/01/01a23cbc8c621642334df9bf655a608bf62546a3a0cbce4ce332e8797d5b68e6.png",
author: "Automate Flows",
},
sidebar: {
image: "https://cdn.21st.dev/assets/mirror/2a/2ac20bd2c1d6309615b71abbc6ef50ec71b6b2b880b6e4f5a45bddc61a1ba2ed.png",
author: "Automate Flows",
},
};
function workspaceRoute(kind: NavigationDemoKind) {
if (typeof location === "undefined") return initialSelection[kind];
const id = location.hash.slice(1);
const available = kind === "sidebar" ? sidebarIds : dockIds;
return available.has(id) ? id : initialSelection[kind];
}
function useWorkspaceTheme() {
const [dark, setDark] = useState(false);
useEffect(() => {
const html = document.documentElement;
const preferred = matchMedia("(prefers-color-scheme: dark)");
const update = () => {
if (html.classList.contains("dark") || html.dataset.theme === "dark") setDark(true);
else if (html.classList.contains("light") || html.dataset.theme === "light") setDark(false);
else setDark(preferred.matches);
};
const changes = new MutationObserver(update);
changes.observe(html, { attributes: true, attributeFilter: ["class", "data-theme"] });
preferred.addEventListener("change", update);
update();
return () => { changes.disconnect(); preferred.removeEventListener("change", update); };
}, []);
return dark;
}
function NavigationWorkspaceDemo({ kind, dark, renderNavigation }: {
kind: NavigationDemoKind;
dark: boolean;
renderNavigation: (bindings: NavigationDemoBindings) => ReactNode;
}) {
const [activeId, setActiveId] = useState(() => workspaceRoute(kind));
const scene = scenes[kind];
useEffect(() => {
const update = () => setActiveId(workspaceRoute(kind));
addEventListener("hashchange", update);
addEventListener("popstate", update);
return () => { removeEventListener("hashchange", update); removeEventListener("popstate", update); };
}, [kind]);
const onNavigate = (item: GlassNavItem, event: MouseEvent< HTMLAnchorElement>) => {
event.preventDefault();
if (location.hash !== item.href) history.pushState(null, "", item.href);
setActiveId(item.id);
};
return (
<main className="gn-workspace" data-scene={kind} data-gn-mode={dark ? "dark" : "light"}>
<style>{workspaceStyles}</style>
<img className="gn-workspace-photo" src={scene.image} alt="" aria-hidden="true" />
<div className="gn-workspace-navigation">
{renderNavigation({ activeId, items: workspaceItems, groups: workspaceGroups, onNavigate })}
</div>
{scene.source ? (
<a className="gn-workspace-credit" href={scene.source} target="_blank" rel="noreferrer">
Photo by {scene.author} <ArrowUpRight aria-hidden="true" />
</a>
) : <span className="gn-workspace-credit">Artwork by {scene.author}</span>}
</main>
);
}
export default function LiquidGlassNotchedDockDemo(props: Partial< typeof settings> = {}) {
const s = { ...settings, ...props };
const dark = useWorkspaceTheme();
const glass = {
blur: s.frost,
opacity: s.opacity,
saturation: s.saturation,
edge: s.edgeLight,
distortion: s.refraction,
tint: s.useHostTheme ? undefined : s.glassTint,
};
return (
<NavigationWorkspaceDemo kind="notched" dark={dark} renderNavigation={({ items, groups, activeId, onNavigate }) => (
<LiquidGlassNotchedDock
items={items}
label="Workspace navigation"
effectIntensity={s.notchDepth}
activeId={activeId}
onNavigate={onNavigate}
glass={glass}
theme={s.useHostTheme ? undefined : dark ? "dark" : "light"}
accent={s.useHostTheme ? undefined : s.accentColor}
effects={{ speed: s.transitionSpeed }}
/>
)} />
);
}
















