starck
componentliquid-glass-navbarsource: ok · hit
Liquid Glass Navbar
SessionWatcher's responsive navbar with expandable or fixed width, real Chromium refraction, and a blur fallback.
npx shadcn add "https://ui.starck.studio/r/liquid-glass-navbar.json"free
Where it goes
navbar
Visual style
glass-blur
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightframer-motion@^11.0.0
Registry dependencies
https://ui.starck.studio/r/blue-cta-button.json
Files2 · source cached 10/8/2026, 7:42:00 PM
registry/default/liquid-glass-navbar.tsx
"use client"
import * as React from "react"
import {
motion,
useReducedMotion,
useScroll,
useTransform,
} from "framer-motion"
import {
AppleMark,
blueCtaClassName,
blueCtaStyle,
} from "@/components/blue-cta-button"
import { createLiquidGlass } from "@/lib/liquid-glass"
type NavLink = {
href: string
label: string
}
type NavbarWidthBehavior = "expand" | "fixed"
type LiquidGlassNavbarProps = {
brand?: string
version?: string
logoSrc?: string
logoAlt?: string
links?: readonly NavLink[]
ctaHref?: string
ctaLabel?: string
widthBehavior?: NavbarWidthBehavior
contained?: boolean
scrollContainerRef?: React.RefObject<HTMLElement | null>
className?: string
}
const defaultLinks: NavLink[] = [
{ href: "#pricing", label: "Pricing" },
{ href: "#features", label: "Features" },
{ href: "#faq", label: "FAQ" },
]
function BrandLogo({
src,
alt,
className,
}: {
src?: string
alt: string
className: string
}) {
if (src) {
return (
// This registry component is framework-neutral, so Next Image is not used.
// eslint-disable-next-line @next/next/no-img-element
<img
src={src}
alt={alt}
className={`${className} rounded-[9px] object-cover`}
/>
)
}
return (
<span
aria-hidden="true"
className={`flex ${className} items-center justify-center rounded-[9px] bg-zinc-950 text-xs font-semibold text-white dark:bg-white dark:text-zinc-950`}
>
S
</span>
)
}
function LiquidGlassNavbar({
brand = "SessionWatcher",
version = "v7.3.0",
logoSrc = "https://sessionwatcher.com/img/NewLogoModernBlack.png",
logoAlt,
links = defaultLinks,
ctaHref = "#",
ctaLabel = "Get SessionWatcher",
widthBehavior = "expand",
contained = false,
scrollContainerRef,
className,
}: LiquidGlassNavbarProps) {
const prefersReducedMotion = useReducedMotion()
const { scrollY } = useScroll({
container: scrollContainerRef,
layoutEffect: false,
})
const animatedWidth = useTransform(scrollY, [0, 400], ["50%", "120%"])
const containedWidth = useTransform(scrollY, [0, 320], ["76%", "100%"])
const mobileNav = React.useRef<HTMLElement>(null)
const desktopNav = React.useRef<HTMLElement>(null)
React.useEffect(() => {
let active = true
let instance: ReturnType<typeof createLiquidGlass> | null = null
const media = window.matchMedia("(min-width: 640px)")
const mount = () => {
instance?.destroy()
instance = null
const element = media.matches ? desktopNav.current : mobileNav.current
if (active && element && element.offsetWidth > 0) {
instance = createLiquidGlass(element, {
borderRadius: Math.round(element.offsetHeight / 2),
blur: 9,
scale: -110,
aberration: [0, 6, 12],
saturation: 1.3,
frost: 0.05,
})
}
}
media.addEventListener("change", mount)
mount()
return () => {
active = false
instance?.destroy()
media.removeEventListener("change", mount)
}
}, [widthBehavior])
const positioning = contained
? "sticky top-4 inset-x-0 h-0"
: "fixed top-4 inset-x-0"
return (
<header
role="banner"
className={[
positioning,
"z-50 flex items-start justify-center px-4 transition-opacity duration-300",
className,
]
.filter(Boolean)
.join(" ")}
>
<nav
ref={mobileNav}
aria-label="Main navigation"
className="block w-full max-w-4xl rounded-full px-4 py-3 backdrop-blur-md sm:hidden"
>
<div className="flex items-center justify-between">
<a href="#" className="flex shrink-0 items-center gap-1.5">
<BrandLogo
src={logoSrc}
alt={logoAlt ?? brand}
className="size-6"
/>
<span className="relative text-lg leading-6 font-bold tracking-tighter whitespace-nowrap">
{brand}
{version ? (
<span className="absolute -top-2 -right-2 rounded-full border border-border/60 bg-foreground/[0.04] px-2 py-0.5 text-[11px] leading-none font-medium text-muted-foreground">
{version}
</span>
) : null}
</span>
</a>
<a
href={ctaHref}
className={`${blueCtaClassName} px-3.5 py-1.5 pl-2.5 text-xs`}
style={blueCtaStyle}
>
<AppleMark className="size-3.5" />
{ctaLabel}
</a>
</div>
</nav>
<motion.nav
ref={desktopNav}
aria-label="Main navigation"
className="hidden max-w-[1200px] rounded-full px-3 py-3 pl-4 backdrop-blur-md sm:block"
style={{
minWidth: contained ? "min(100%, 44rem)" : undefined,
width:
widthBehavior === "fixed"
? "100%"
: contained
? prefersReducedMotion
? "100%"
: containedWidth
: prefersReducedMotion
? "85%"
: animatedWidth,
}}
>
<div className="relative flex items-center justify-between">
<a href="#" className="flex shrink-0 items-center gap-2">
<BrandLogo
src={logoSrc}
alt={logoAlt ?? brand}
className="size-8"
/>
<span className="relative text-2xl leading-8 font-bold tracking-tighter whitespace-nowrap">
{brand}
{version ? (
<span className="absolute -top-2.5 -right-2 rounded-full border border-border/60 bg-foreground/[0.04] px-2 py-0.5 text-[12px] leading-none font-medium text-muted-foreground">
{version}
</span>
) : null}
</span>
</a>
<div className="absolute left-1/2 flex -translate-x-1/2 items-center gap-5">
{links.map((link) => (
<a
key={`${link.href}-${link.label}`}
href={link.href}
className="text-sm font-medium text-muted-foreground transition-colors hover:text-foreground"
>
{link.label}
</a>
))}
</div>
<a
href={ctaHref}
className={`${blueCtaClassName} shrink-0`}
style={blueCtaStyle}
>
<AppleMark className="size-[15px]" />
{ctaLabel}
</a>
</div>
</motion.nav>
</header>
)
}
export { LiquidGlassNavbar }
export type { LiquidGlassNavbarProps, NavbarWidthBehavior, NavLink }
registry/default/liquid-glass.ts
/**
* Liquid Glass: optical refraction for the web.
* Adapted from https://github.com/rizzytoday/liquid-glass
* Chromium uses SVG displacement; other browsers receive backdrop blur.
*
* MIT License
* Copyright (c) 2026 Riz Roze
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
type LiquidGlassOptions = {
width?: number
height?: number
borderRadius?: number
scale?: number
border?: number
lightness?: number
alpha?: number
blur?: number
aberration?: [number, number, number]
frost?: number
saturation?: number
displaceBlur?: number
fallbackFilter?: string
filterId?: string
}
type LiquidGlassConfig = {
width: number
height: number
radius: number
scale: number
border: number
lightness: number
alpha: number
blur: number
r: number
g: number
b: number
frost: number
saturation: number
displace: number
}
type FilterRefs = {
svg: SVGSVGElement
filter: SVGFilterElement
image: SVGFEImageElement
red: SVGFEDisplacementMapElement
green: SVGFEDisplacementMapElement
blue: SVGFEDisplacementMapElement
blur: SVGFEGaussianBlurElement
}
type LiquidGlassInstance = {
isActive: boolean
filterElement: SVGSVGElement
update: (options: Partial<LiquidGlassOptions>) => void
destroy: () => void
}
const isChromium =
typeof navigator !== "undefined" && /Chrome\//.test(navigator.userAgent)
const mapCache = new Map<string, string>()
let instanceCount = 0
function resolveConfig(
element: HTMLElement,
options: LiquidGlassOptions
): LiquidGlassConfig {
const rect = element.getBoundingClientRect()
const aberration = options.aberration ?? [0, 10, 20]
return {
width: options.width ?? Math.round(rect.width),
height: options.height ?? Math.round(rect.height),
radius: options.borderRadius ?? 50,
scale: options.scale ?? -180,
border: options.border ?? 0.07,
lightness: options.lightness ?? 50,
alpha: options.alpha ?? 0.93,
blur: options.blur ?? 11,
r: aberration[0],
g: aberration[1],
b: aberration[2],
frost: options.frost ?? 0,
saturation: options.saturation ?? 1,
displace: options.displaceBlur ?? 0,
}
}
function buildDisplacementMap(config: LiquidGlassConfig) {
const key = [
config.width,
config.height,
config.radius,
config.scale,
config.border,
config.blur,
config.lightness,
config.alpha,
].join(":")
const cached = mapCache.get(key)
if (cached) return cached
const maxDisplace = Math.max(Math.abs(config.scale) * 0.5, 20)
const padding = Math.ceil(maxDisplace)
const canvas = document.createElement("canvas")
canvas.width = config.width + padding * 2
canvas.height = config.height + padding * 2
const context = canvas.getContext("2d")
if (!context) throw new Error("Liquid Glass needs Canvas 2D support.")
context.fillStyle = "rgb(128, 128, 128)"
context.fillRect(0, 0, canvas.width, canvas.height)
context.save()
context.beginPath()
context.roundRect(
padding,
padding,
config.width,
config.height,
config.radius
)
context.clip()
context.fillStyle = "#000"
context.fillRect(padding, padding, config.width, config.height)
const red = context.createLinearGradient(
padding + config.width,
padding,
padding,
padding
)
red.addColorStop(0, "#000")
red.addColorStop(1, "#f00")
context.fillStyle = red
context.fillRect(padding, padding, config.width, config.height)
context.globalCompositeOperation = "difference"
const blue = context.createLinearGradient(
padding,
padding,
padding,
padding + config.height
)
blue.addColorStop(0, "#000")
blue.addColorStop(1, "#00f")
context.fillStyle = blue
context.fillRect(padding, padding, config.width, config.height)
context.globalCompositeOperation = "source-over"
const border = Math.min(config.width, config.height) * (config.border * 0.5)
context.filter = `blur(${config.blur}px)`
context.fillStyle = `hsla(0, 0%, ${config.lightness}%, ${config.alpha})`
context.beginPath()
context.roundRect(
padding + border,
padding + border,
config.width - border * 2,
config.height - border * 2,
config.radius
)
context.fill()
context.restore()
const uri = canvas.toDataURL()
mapCache.set(key, uri)
return uri
}
function createFilterSvg(id: string): FilterRefs {
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg")
svg.style.cssText = "position:absolute;width:0;height:0;pointer-events:none"
svg.innerHTML = `
<defs>
<filter id="${id}" color-interpolation-filters="sRGB">
<feImage result="map" preserveAspectRatio="none" />
<feDisplacementMap in="SourceGraphic" in2="map" xChannelSelector="R" yChannelSelector="B" result="dispRed" data-channel="red" />
<feColorMatrix in="dispRed" type="matrix" values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" result="red" />
<feDisplacementMap in="SourceGraphic" in2="map" xChannelSelector="R" yChannelSelector="B" result="dispGreen" data-channel="green" />
<feColorMatrix in="dispGreen" type="matrix" values="0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0" result="green" />
<feDisplacementMap in="SourceGraphic" in2="map" xChannelSelector="R" yChannelSelector="B" result="dispBlue" data-channel="blue" />
<feColorMatrix in="dispBlue" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0" result="blue" />
<feBlend in="red" in2="green" mode="screen" result="rg" />
<feBlend in="rg" in2="blue" mode="screen" result="output" />
<feGaussianBlur in="output" stdDeviation="0" />
</filter>
</defs>`
const get = <T extends Element>(selector: string) => {
const node = svg.querySelector<T>(selector)
if (!node) throw new Error(`Liquid Glass filter is missing ${selector}.`)
return node
}
return {
svg,
filter: get<SVGFilterElement>("filter"),
image: get<SVGFEImageElement>("feImage"),
red: get<SVGFEDisplacementMapElement>('[data-channel="red"]'),
green: get<SVGFEDisplacementMapElement>('[data-channel="green"]'),
blue: get<SVGFEDisplacementMapElement>('[data-channel="blue"]'),
blur: get<SVGFEGaussianBlurElement>("feGaussianBlur"),
}
}
function applyConfig(config: LiquidGlassConfig, refs: FilterRefs) {
const uri = buildDisplacementMap(config)
const maxDisplace = Math.max(Math.abs(config.scale) * 0.5, 20)
const x = Math.ceil((maxDisplace / config.width) * 100)
const y = Math.ceil((maxDisplace / config.height) * 100)
refs.filter.setAttribute("x", `-${x}%`)
refs.filter.setAttribute("y", `-${y}%`)
refs.filter.setAttribute("width", `${100 + x * 2}%`)
refs.filter.setAttribute("height", `${100 + y * 2}%`)
refs.image.setAttributeNS("http://www.w3.org/1999/xlink", "href", uri)
refs.image.setAttribute("href", uri)
refs.red.setAttribute("scale", String(config.scale + config.r))
refs.green.setAttribute("scale", String(config.scale + config.g))
refs.blue.setAttribute("scale", String(config.scale + config.b))
refs.blur.setAttribute("stdDeviation", String(config.displace))
}
function createLiquidGlass(
element: HTMLElement,
options: LiquidGlassOptions = {}
): LiquidGlassInstance {
const fallback = options.fallbackFilter ?? "blur(12px)"
if (!isChromium) {
const previous = element.style.backdropFilter
const previousWebkit = element.style.getPropertyValue(
"-webkit-backdrop-filter"
)
element.style.backdropFilter = fallback
element.style.setProperty("-webkit-backdrop-filter", fallback)
const placeholder = document.createElementNS(
"http://www.w3.org/2000/svg",
"svg"
)
return {
isActive: false,
filterElement: placeholder,
update() {},
destroy() {
element.style.backdropFilter = previous
element.style.setProperty("-webkit-backdrop-filter", previousWebkit)
},
}
}
const id = options.filterId ?? `liquid-glass-${++instanceCount}`
const refs = createFilterSvg(id)
document.body.appendChild(refs.svg)
let currentOptions = { ...options }
let config = resolveConfig(element, currentOptions)
const applyStyles = (nextConfig: LiquidGlassConfig) => {
const filter = `url(#${id}) saturate(${nextConfig.saturation})`
element.style.backdropFilter = filter
element.style.setProperty("-webkit-backdrop-filter", filter)
if (nextConfig.frost > 0) {
element.style.background = `hsl(0 0% 0% / ${nextConfig.frost})`
}
}
applyConfig(config, refs)
applyStyles(config)
let resizeFrame = 0
const observer = new ResizeObserver(() => {
cancelAnimationFrame(resizeFrame)
resizeFrame = requestAnimationFrame(() => {
if (currentOptions.width == null || currentOptions.height == null) {
config = resolveConfig(element, currentOptions)
applyConfig(config, refs)
applyStyles(config)
}
})
})
observer.observe(element)
return {
isActive: true,
filterElement: refs.svg,
update(nextOptions) {
currentOptions = { ...currentOptions, ...nextOptions }
config = resolveConfig(element, currentOptions)
applyConfig(config, refs)
applyStyles(config)
},
destroy() {
observer.disconnect()
cancelAnimationFrame(resizeFrame)
refs.svg.remove()
element.style.backdropFilter = ""
element.style.setProperty("-webkit-backdrop-filter", "")
element.style.background = ""
},
}
}
export { createLiquidGlass, isChromium }
export type { LiquidGlassInstance, LiquidGlassOptions }