Automate Flows w/ Troy

Liquid Glass Notched Dock

Navigation

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 }}
      />
    )} />
  );
}

Dependencies

More by Automate Flows w/ Troy

View all
  • Adjustable Glass Dashboard

    Adjustable Glass Dashboard

    696
  • Glassy Coverflow Carousel

    Glassy Coverflow Carousel

    301
  • Interactive Workflow Blueprint

    Interactive Workflow Blueprint

    270
  • Dimensional Workflow Hero

    Dimensional Workflow Hero

    120
  • Liquid Glass Wordmark Footer

    Liquid Glass Wordmark Footer

    96
  • Liquid Glass Zoom Navigation

    Liquid Glass Zoom Navigation

    1
  • Spatial Orbit Gallery

    Spatial Orbit Gallery

    1
  • Daytake Landing Page

    Daytake Landing Page

More in Navigation

View all
  • Sidebar

    Sidebar

    360
  • Pagination

    Pagination

    37
  • Navbar

    Navbar

    8
  • Oversized 404 Numeral

    Oversized 404 Numeral

    72
  • Footer

    Footer

    456
  • Sortable Sidebar Navigation

    Sortable Sidebar Navigation

    75
  • Tabs like bookmark

    Tabs like bookmark

    75
  • Navbar with Theme Toggle

    Navbar with Theme Toggle

    58