Original by Plinth · MIT
Buttons
Glass Button
A Liquid Glass pill that refracts the colour beneath it, with a specular rim and a highlight that tracks your pointer.
Install
npx shadcn@latest add https://plinthui.com/r/glass-button.json'use client';
import { useRef } from 'react';
// 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-btn {
position: relative;
isolation: isolate;
box-sizing: border-box;
display: inline-flex;
align-items: center;
gap: 9px;
padding: 15px 30px;
border: 0;
border-radius: 999px;
font: inherit;
font-size: 16px;
font-weight: 600;
letter-spacing: -0.005em;
color: #fff;
text-shadow: 0 1px 3px rgba(30, 8, 40, 0.5);
cursor: pointer;
-webkit-tap-highlight-color: transparent;
background:
radial-gradient(circle 130px at var(--mx) var(--my), rgba(255, 255, 255, 0.44), rgba(255, 255, 255, 0.07) 55%, transparent 76%),
linear-gradient(148deg, rgba(255, 255, 255, 0.3) 0%, rgba(255, 255, 255, 0.09) 46%, rgba(255, 255, 255, 0.2) 100%);
-webkit-backdrop-filter: blur(14px) saturate(195%) brightness(1.12);
backdrop-filter: blur(14px) saturate(195%) brightness(1.12);
box-shadow:
inset 0 1px 1px rgba(255, 255, 255, 0.7),
inset 0 -1px 1px rgba(255, 255, 255, 0.28),
inset 0 0 18px -6px rgba(255, 255, 255, 0.45),
0 2px 5px rgba(34, 8, 44, 0.22),
0 10px 20px -8px rgba(34, 8, 44, 0.36),
0 22px 40px -20px rgba(34, 8, 44, 0.5);
transition:
transform 220ms cubic-bezier(0.34, 1.4, 0.5, 1),
box-shadow 260ms ease;
}
.glass-btn svg { width: 17px; height: 17px; display: block; }
.glass-btn::before {
content: "";
position: absolute;
inset: 0;
padding: 1.5px;
border-radius: inherit;
pointer-events: none;
z-index: 2;
background: linear-gradient(
148deg,
rgba(255, 255, 255, 1) 0%,
rgba(255, 255, 255, 0.55) 18%,
rgba(255, 255, 255, 0.07) 44%,
rgba(255, 255, 255, 0.07) 62%,
rgba(255, 255, 255, 0.85) 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-btn::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
z-index: 1;
opacity: 0;
background: linear-gradient(102deg, transparent 34%, rgba(255, 255, 255, 0.55) 50%, transparent 66%);
background-size: 260% 100%;
background-position: 150% 0;
}
.glass-btn:hover {
transform: translateY(-1.5px);
box-shadow:
inset 0 1px 1px rgba(255, 255, 255, 0.85),
inset 0 -1px 1px rgba(255, 255, 255, 0.34),
inset 0 0 20px -5px rgba(255, 255, 255, 0.6),
0 3px 7px rgba(34, 8, 44, 0.24),
0 14px 26px -8px rgba(34, 8, 44, 0.4),
0 30px 52px -22px rgba(34, 8, 44, 0.55);
}
.glass-btn:hover::after {
animation: glass-btn-sweep 900ms cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes glass-btn-sweep {
0% { opacity: 0; background-position: 150% 0; }
18% { opacity: 1; }
100% { opacity: 0; background-position: -70% 0; }
}
.glass-btn:active {
transform: translateY(0) scale(0.972);
box-shadow:
inset 0 1px 1px rgba(255, 255, 255, 0.5),
inset 0 6px 14px -8px rgba(30, 8, 40, 0.42),
inset 0 0 14px -6px rgba(255, 255, 255, 0.3),
0 1px 3px rgba(34, 8, 44, 0.24),
0 5px 12px -6px rgba(34, 8, 44, 0.3);
transition: transform 90ms ease-out, box-shadow 90ms ease-out;
}
.glass-btn:focus-visible {
outline: 2px solid rgba(255, 255, 255, 0.95);
outline-offset: 3px;
}
`;
const REST = { mx: '30%', my: '0%' };
export function GlassButton({
children = 'Continue',
onClick,
}: {
children?: React.ReactNode;
onClick?: () => void;
}) {
const btnRef = useRef<HTMLButtonElement>(null);
// Written straight to the element so the pointer never triggers a re-render.
const setHighlight = (x: string, y: string) => {
btnRef.current?.style.setProperty('--mx', x);
btnRef.current?.style.setProperty('--my', y);
};
const handlePointerMove = (event: React.PointerEvent<HTMLButtonElement>) => {
const rect = event.currentTarget.getBoundingClientRect();
setHighlight(`${event.clientX - rect.left}px`, `${event.clientY - rect.top}px`);
};
return (
<>
<style>{css}</style>
<button
ref={btnRef}
type="button"
className="glass-btn"
style={{ '--mx': REST.mx, '--my': REST.my } as React.CSSProperties}
onPointerMove={handlePointerMove}
onPointerLeave={() => setHighlight(REST.mx, REST.my)}
onClick={onClick}
>
{children}
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M4.5 12h14" />
<path d="m12.8 5.8 6.2 6.2-6.2 6.2" />
</svg>
</button>
</>
);
}