Liquid Glass Button
A frosted backdrop-blur button with layered inset highlights, an animated conic-gradient border, a moving specular sheen, and a shadow that tilts on press. Ported from Petr Knoll.
hover for the sheen · press to tilt
Usage
import { GlassButton } from "~/components/buttons/glass-button";<GlassButton onClick={generate}>Generate</GlassButton>
What's going on
This is Petr Knoll's button, and it's a small masterclass in stacking cheap effects. The pill is translucent with a backdrop-filter: blur, so it actually frosts whatever's behind it (that's why it wants a busy, light background to sit on). Two inset shadows, a dark one up top and a light one along the bottom, give it a glassy edge.
The two moving parts both lean on @property. CSS can't normally animate the angle inside a gradient, but registering an angle custom property as <angle> makes it interpolatable. So the conic-gradient border (masked down to a hairline with mask-composite: exclude) and the diagonal sheen across the face both rotate smoothly on hover and press. A separate blurred element underneath is the shadow, and on :active the whole wrap tilts back with rotate3d so it feels like it's being pushed.
It's all em-based, so it scales with font-size, and it degrades on touch (the angle animations freeze rather than jump).
/* Animatable angles, so the border + sheen can transition smoothly. */@property --glass-1 { syntax: "<angle>"; inherits: false; initial-value: -75deg; }@property --glass-2 { syntax: "<angle>"; inherits: false; initial-value: -45deg; }.glass-btn {all: unset;background: linear-gradient(-75deg,rgba(255,255,255,.05), rgba(255,255,255,.2), rgba(255,255,255,.05));backdrop-filter: blur(clamp(1px, .125em, 4px)); /* the frost */box-shadow:inset 0 .125em .125em rgba(0,0,0,.05),inset 0 -.125em .125em rgba(255,255,255,.5), /* bottom rim light */0 .25em .125em -.125em rgba(0,0,0,.2); /* soft lift */}/* Conic-gradient border masked to a hairline; the angle animates on hover. */.glass-btn::after {background: conic-gradient(from var(--glass-1) at 50% 50%, …);mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);mask-composite: exclude;}.glass-btn:hover::after { --glass-1: -125deg; }
Props
| prop | type | default | description |
|---|---|---|---|
| children | ReactNode | "Generate" | Button label. |
| …props | ButtonHTMLAttributes | — | Any native button prop (onClick, type, aria-*). |
Original by Petr Knoll.
Source
download .zipThe full implementation, across 2 files. Copy it in or grab the zip. It leans on a cn() class helper and the theme tokens (--primary, --border, …).
import { cn } from "~/lib/utils";export interface GlassButtonPropsextends React.ButtonHTMLAttributes<HTMLButtonElement> {}/*** A frosted "liquid glass" button: a translucent `backdrop-filter` pill with* layered inset highlights, an animated conic gradient border and a moving* specular sheen (both via `@property` angle transitions), and a separate* blurred shadow element that lifts and tilts on press. Reads best on a light,* slightly busy background. Styling lives in `.glass-btn-*` (app.css).* Ported from Petr Knoll's CodePen.*/export function GlassButton({children = "Generate",className,type = "button",...props}: GlassButtonProps) {return (<span className={cn("glass-btn-wrap", className)}><button type={type} className="glass-btn" {...props}><span>{children}</span></button><span className="glass-btn-shadow" aria-hidden="true" /></span>);}
/* ─── Liquid-glass button, ported from Petr Knoll ──────────────────── */@property --glass-1 {syntax: "<angle>";inherits: false;initial-value: -75deg;}@property --glass-2 {syntax: "<angle>";inherits: false;initial-value: -45deg;}.glass-btn-wrap {--anim--hover-time: 400ms;--anim--hover-ease: cubic-bezier(0.25, 1, 0.5, 1);position: relative;z-index: 2;display: inline-block;border-radius: 999vw;pointer-events: none;transition: all var(--anim--hover-time) var(--anim--hover-ease);}.glass-btn-shadow {--shadow-cuttoff-fix: 2em;position: absolute;width: calc(100% + var(--shadow-cuttoff-fix));height: calc(100% + var(--shadow-cuttoff-fix));top: calc(0% - var(--shadow-cuttoff-fix) / 2);left: calc(0% - var(--shadow-cuttoff-fix) / 2);filter: blur(clamp(2px, 0.125em, 12px));overflow: visible;pointer-events: none;}.glass-btn-shadow::after {content: "";position: absolute;z-index: 0;inset: 0;border-radius: 999vw;background: linear-gradient(180deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.1));width: calc(100% - var(--shadow-cuttoff-fix) - 0.25em);height: calc(100% - var(--shadow-cuttoff-fix) - 0.25em);top: calc(var(--shadow-cuttoff-fix) - 0.5em);left: calc(var(--shadow-cuttoff-fix) - 0.875em);padding: 0.125em;box-sizing: border-box;mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);mask-composite: exclude;transition: all var(--anim--hover-time) var(--anim--hover-ease);overflow: visible;opacity: 1;}.glass-btn {--border-width: clamp(1px, 0.0625em, 4px);all: unset;cursor: pointer;position: relative;-webkit-tap-highlight-color: rgba(0, 0, 0, 0);pointer-events: auto;z-index: 3;font-size: clamp(1.1rem, 2.6vw, 1.6rem);background: linear-gradient(-75deg,rgba(255, 255, 255, 0.05),rgba(255, 255, 255, 0.2),rgba(255, 255, 255, 0.05));border-radius: 999vw;box-shadow:inset 0 0.125em 0.125em rgba(0, 0, 0, 0.05),inset 0 -0.125em 0.125em rgba(255, 255, 255, 0.5),0 0.25em 0.125em -0.125em rgba(0, 0, 0, 0.2),0 0 0.1em 0.25em inset rgba(255, 255, 255, 0.2),0 0 0 0 rgba(255, 255, 255, 1);backdrop-filter: blur(clamp(1px, 0.125em, 4px));-webkit-backdrop-filter: blur(clamp(1px, 0.125em, 4px));transition: all var(--anim--hover-time) var(--anim--hover-ease);}.glass-btn:hover {transform: scale(0.975);backdrop-filter: blur(0.01em);-webkit-backdrop-filter: blur(0.01em);box-shadow:inset 0 0.125em 0.125em rgba(0, 0, 0, 0.05),inset 0 -0.125em 0.125em rgba(255, 255, 255, 0.5),0 0.15em 0.05em -0.1em rgba(0, 0, 0, 0.25),0 0 0.05em 0.1em inset rgba(255, 255, 255, 0.5),0 0 0 0 rgba(255, 255, 255, 1);}.glass-btn span {position: relative;display: block;user-select: none;letter-spacing: -0.05em;font-weight: 500;font-size: 1em;color: rgba(50, 50, 50, 1);text-shadow: 0em 0.25em 0.05em rgba(0, 0, 0, 0.1);transition: all var(--anim--hover-time) var(--anim--hover-ease);padding-inline: 1.5em;padding-block: 0.875em;}.glass-btn:hover span {text-shadow: 0.025em 0.025em 0.025em rgba(0, 0, 0, 0.12);}.glass-btn span::after {content: "";display: block;position: absolute;z-index: 1;width: calc(100% - var(--border-width));height: calc(100% - var(--border-width));top: calc(0% + var(--border-width) / 2);left: calc(0% + var(--border-width) / 2);box-sizing: border-box;border-radius: 999vw;overflow: clip;background: linear-gradient(var(--glass-2),rgba(255, 255, 255, 0) 0%,rgba(255, 255, 255, 0.5) 40% 50%,rgba(255, 255, 255, 0) 55%);z-index: 3;mix-blend-mode: screen;pointer-events: none;background-size: 200% 200%;background-position: 0% 50%;background-repeat: no-repeat;transition:background-position calc(var(--anim--hover-time) * 1.25) var(--anim--hover-ease),--glass-2 calc(var(--anim--hover-time) * 1.25) var(--anim--hover-ease);}.glass-btn:hover span::after {background-position: 25% 50%;}.glass-btn:active span::after {background-position: 50% 15%;--glass-2: -15deg;}.glass-btn::after {content: "";position: absolute;z-index: 1;inset: 0;border-radius: 999vw;width: calc(100% + var(--border-width));height: calc(100% + var(--border-width));top: calc(0% - var(--border-width) / 2);left: calc(0% - var(--border-width) / 2);padding: var(--border-width);box-sizing: border-box;background: conic-gradient(from var(--glass-1) at 50% 50%,rgba(0, 0, 0, 0.5),rgba(0, 0, 0, 0) 5% 40%,rgba(0, 0, 0, 0.5) 50%,rgba(0, 0, 0, 0) 60% 95%,rgba(0, 0, 0, 0.5)),linear-gradient(180deg, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5));mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);mask-composite: exclude;transition:all var(--anim--hover-time) var(--anim--hover-ease),--glass-1 500ms ease;box-shadow: inset 0 0 0 calc(var(--border-width) / 2) rgba(255, 255, 255, 0.5);}.glass-btn:hover::after {--glass-1: -125deg;}.glass-btn:active::after {--glass-1: -75deg;}.glass-btn-wrap:has(.glass-btn:hover) .glass-btn-shadow {filter: blur(clamp(2px, 0.0625em, 6px));}.glass-btn-wrap:has(.glass-btn:hover) .glass-btn-shadow::after {top: calc(var(--shadow-cuttoff-fix) - 0.875em);opacity: 1;}.glass-btn-wrap:has(.glass-btn:active) {transform: rotate3d(1, 0, 0, 25deg);}.glass-btn-wrap:has(.glass-btn:active) .glass-btn {box-shadow:inset 0 0.125em 0.125em rgba(0, 0, 0, 0.05),inset 0 -0.125em 0.125em rgba(255, 255, 255, 0.5),0 0.125em 0.125em -0.125em rgba(0, 0, 0, 0.2),0 0 0.1em 0.25em inset rgba(255, 255, 255, 0.2),0 0.225em 0.05em 0 rgba(0, 0, 0, 0.05),0 0.25em 0 0 rgba(255, 255, 255, 0.75),inset 0 0.25em 0.05em 0 rgba(0, 0, 0, 0.15);}.glass-btn-wrap:has(.glass-btn:active) span {text-shadow: 0.025em 0.25em 0.05em rgba(0, 0, 0, 0.12);}@media (hover: none) and (pointer: coarse) {.glass-btn span::after,.glass-btn:active span::after {--glass-2: -45deg;}.glass-btn::after,.glass-btn:hover::after,.glass-btn:active::after {--glass-1: -75deg;}}