Glass
import { useEffect, useRef } from 'react';
const css = `
.btn-glass {
position: relative;
display: grid;
place-items: center;
width: 260px;
height: 150px;
}
.btn-glass__light {
position: absolute;
width: 74px;
height: 74px;
margin: -37px 0 0 -37px;
left: 0;
top: 0;
border-radius: 50%;
filter: blur(6px);
will-change: transform;
pointer-events: none;
}
.btn-glass__light:nth-child(1) { background: light-dark(#373a40, #f1f2f3); opacity: 0.35; }
.btn-glass__light:nth-child(2) { background: light-dark(#5f626a, #a7abb2); opacity: 0.55; width: 56px; height: 56px; margin: -28px 0 0 -28px; }
.btn-glass__light:nth-child(3) { background: light-dark(#42454c, #e5e6e9); opacity: 0.3; width: 90px; height: 40px; margin: -20px 0 0 -45px; }
.btn-glass__pane {
--hx: 50%;
--hy: 0%;
position: relative;
padding: 1rem 2.4rem;
border: none;
border-radius: 18px;
background: color-mix(in oklab, light-dark(#373a40, #f1f2f3) 7%, transparent);
backdrop-filter: blur(16px) saturate(1.4);
-webkit-backdrop-filter: blur(16px) saturate(1.4);
box-shadow:
inset 0 1px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 35%, transparent),
inset 0 0 0 1px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 14%, transparent),
0 16px 32px -14px light-dark(rgba(0, 0, 0, 0.17), rgba(0, 0, 0, 0.55));
color: light-dark(#373a40, #f1f2f3);
text-shadow: 0 1px 8px light-dark(#fbfcfd, #121417);
font-family: inherit;
font-size: 15px;
font-weight: 600;
cursor: pointer;
overflow: hidden;
transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}
/* A rim light that slides round the edge toward the cursor. */
.btn-glass__pane::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
padding: 1px;
background: radial-gradient(circle 70px at var(--hx) var(--hy), color-mix(in oklab, light-dark(#373a40, #f1f2f3) 80%, transparent), transparent);
mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
pointer-events: none;
}
.btn-glass__pane:hover { transform: translateY(-1px); }
.btn-glass__pane:active { transform: scale(0.98); }
.btn-glass__pane:focus-visible { outline: 2px solid light-dark(#42454c, #e5e6e9); outline-offset: 3px; }
`;
// Resting orbits for the three lights (centre offsets in px, radius, speed).
const ORBITS = [
{ cx: -50, cy: -10, r: 26, s: 0.5 },
{ cx: 46, cy: 14, r: 22, s: -0.7 },
{ cx: 0, cy: 22, r: 30, s: 0.35 },
];
export function GlassButton() {
const root = useRef<HTMLDivElement>(null);
const pane = useRef<HTMLButtonElement>(null);
const lights = useRef<HTMLSpanElement[]>([]);
const pointer = useRef<{ x: number; y: number } | null>(null);
useEffect(() => {
const still = matchMedia('(prefers-reduced-motion: reduce)').matches;
const el = root.current!;
const pos = ORBITS.map(() => ({ x: el.clientWidth / 2, y: el.clientHeight / 2 }));
let raf = 0;
const tick = (now: number) => {
const t = now / 1000;
const w = el.clientWidth;
const h = el.clientHeight;
ORBITS.forEach((o, i) => {
const home = { x: w / 2 + o.cx + Math.cos(t * o.s + i) * o.r, y: h / 2 + o.cy + Math.sin(t * o.s * 1.3 + i) * o.r * 0.6 };
// Each light gathers toward the cursor, staggered so they arrive one after another.
const p = pointer.current;
const target = p ? { x: home.x * 0.35 + p.x * 0.65, y: home.y * 0.35 + p.y * 0.65 } : home;
const k = 0.04 + i * 0.02;
pos[i].x += (target.x - pos[i].x) * k;
pos[i].y += (target.y - pos[i].y) * k;
lights.current[i].style.transform = `translate(${pos[i].x.toFixed(1)}px, ${pos[i].y.toFixed(1)}px)`;
});
if (!still) raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, []);
return (
<>
<style>
{css}
</style>
<div
ref={root}
className="btn-glass"
onPointerMove={(e) => {
const r = root.current!.getBoundingClientRect();
pointer.current = { x: e.clientX - r.left, y: e.clientY - r.top };
const b = pane.current!.getBoundingClientRect();
pane.current!.style.setProperty('--hx', `${e.clientX - b.left}px`);
pane.current!.style.setProperty('--hy', `${e.clientY - b.top}px`);
}}
onPointerLeave={() => (pointer.current = null)}
>
{ORBITS.map((_, i) => (
<span key={i} ref={(el) => void (el && (lights.current[i] = el))} className="btn-glass__light" aria-hidden="true" />
))}
<button ref={pane} type="button" className="btn-glass__pane">
Continue
</button>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initBtnGlass(el) on the mounted element instead.
<div class="btn-glass">
<span class="btn-glass__light" aria-hidden="true"></span>
<span class="btn-glass__light" aria-hidden="true"></span>
<span class="btn-glass__light" aria-hidden="true"></span>
<button type="button" class="btn-glass__pane">Continue</button>
</div>.btn-glass {
position: relative;
display: grid;
place-items: center;
width: 260px;
height: 150px;
}
.btn-glass__light {
position: absolute;
width: 74px;
height: 74px;
margin: -37px 0 0 -37px;
left: 0;
top: 0;
border-radius: 50%;
filter: blur(6px);
will-change: transform;
pointer-events: none;
}
.btn-glass__light:nth-child(1) { background: light-dark(#373a40, #f1f2f3); opacity: 0.35; }
.btn-glass__light:nth-child(2) { background: light-dark(#5f626a, #a7abb2); opacity: 0.55; width: 56px; height: 56px; margin: -28px 0 0 -28px; }
.btn-glass__light:nth-child(3) { background: light-dark(#42454c, #e5e6e9); opacity: 0.3; width: 90px; height: 40px; margin: -20px 0 0 -45px; }
.btn-glass__pane {
--hx: 50%;
--hy: 0%;
position: relative;
padding: 1rem 2.4rem;
border: none;
border-radius: 18px;
background: color-mix(in oklab, light-dark(#373a40, #f1f2f3) 7%, transparent);
backdrop-filter: blur(16px) saturate(1.4);
-webkit-backdrop-filter: blur(16px) saturate(1.4);
box-shadow:
inset 0 1px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 35%, transparent),
inset 0 0 0 1px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 14%, transparent),
0 16px 32px -14px light-dark(rgba(0, 0, 0, 0.17), rgba(0, 0, 0, 0.55));
color: light-dark(#373a40, #f1f2f3);
text-shadow: 0 1px 8px light-dark(#fbfcfd, #121417);
font-family: inherit;
font-size: 15px;
font-weight: 600;
cursor: pointer;
overflow: hidden;
transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}
/* A rim light that slides round the edge toward the cursor. */
.btn-glass__pane::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
padding: 1px;
background: radial-gradient(circle 70px at var(--hx) var(--hy), color-mix(in oklab, light-dark(#373a40, #f1f2f3) 80%, transparent), transparent);
mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
pointer-events: none;
}
.btn-glass__pane:hover { transform: translateY(-1px); }
.btn-glass__pane:active { transform: scale(0.98); }
.btn-glass__pane:focus-visible { outline: 2px solid light-dark(#42454c, #e5e6e9); outline-offset: 3px; }function initBtnGlass(root) {
var pane = root.querySelector('.btn-glass__pane');
var lights = root.querySelectorAll('.btn-glass__light');
if (!pane || lights.length < 3) return;
// Resting orbits for the three lights (centre offsets in px, radius, speed).
var ORBITS = [
{ cx: -50, cy: -10, r: 26, s: 0.5 },
{ cx: 46, cy: 14, r: 22, s: -0.7 },
{ cx: 0, cy: 22, r: 30, s: 0.35 },
];
var still = matchMedia('(prefers-reduced-motion: reduce)').matches;
var pos = ORBITS.map(function () { return { x: root.clientWidth / 2, y: root.clientHeight / 2 }; });
var pointer = null;
var raf = 0;
function tick(now) {
var t = now / 1000;
var w = root.clientWidth;
var h = root.clientHeight;
ORBITS.forEach(function (o, i) {
var home = { x: w / 2 + o.cx + Math.cos(t * o.s + i) * o.r, y: h / 2 + o.cy + Math.sin(t * o.s * 1.3 + i) * o.r * 0.6 };
// Each light gathers toward the cursor, staggered so they arrive one after another.
var target = pointer ? { x: home.x * 0.35 + pointer.x * 0.65, y: home.y * 0.35 + pointer.y * 0.65 } : home;
var k = 0.04 + i * 0.02;
pos[i].x += (target.x - pos[i].x) * k;
pos[i].y += (target.y - pos[i].y) * k;
lights[i].style.transform = 'translate(' + pos[i].x.toFixed(1) + 'px, ' + pos[i].y.toFixed(1) + 'px)';
});
if (!still) raf = requestAnimationFrame(tick);
}
function move(e) {
var r = root.getBoundingClientRect();
pointer = { x: e.clientX - r.left, y: e.clientY - r.top };
var b = pane.getBoundingClientRect();
pane.style.setProperty('--hx', e.clientX - b.left + 'px');
pane.style.setProperty('--hy', e.clientY - b.top + 'px');
}
function leave() { pointer = null; }
root.addEventListener('pointermove', move);
root.addEventListener('pointerleave', leave);
raf = requestAnimationFrame(tick);
return function () {
cancelAnimationFrame(raf);
root.removeEventListener('pointermove', move);
root.removeEventListener('pointerleave', leave);
};
}
document.querySelectorAll('.btn-glass').forEach(initBtnGlass);Goal: Create a "Glass" button.
Visual behavior: A frosted glass pane floating over drifting soft lights. Move the cursor around it and the lights behind the glass gather toward the pointer one after another, while a rim light slides round the pane's edge to face it.
Technique:
- Three blurred light blobs orbit slowly behind the button; the button is a translucent pane with backdrop-filter: blur(16px) saturate(1.4), an inner top highlight and a soft drop shadow.
- Each frame every light eases toward a target: its orbit point at rest, or 65% of the way to the cursor while hovered. Each has a different easing factor, so they arrive one after another.
- The rim is a ::before radial gradient at the cursor, masked to a 1px ring with a content-box exclude mask, so only the edge nearest the pointer lights.
Accessibility: A real <button> with a visible label; decoration is aria-hidden. Focus outline. Under prefers-reduced-motion the lights hold their positions.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Glass" button.
Visual behavior: A frosted glass pane floating over drifting soft lights. Move the cursor around it and the lights behind the glass gather toward the pointer one after another, while a rim light slides round the pane's edge to face it.
Technique:
- Three blurred light blobs orbit slowly behind the button; the button is a translucent pane with backdrop-filter: blur(16px) saturate(1.4), an inner top highlight and a soft drop shadow.
- Each frame every light eases toward a target: its orbit point at rest, or 65% of the way to the cursor while hovered. Each has a different easing factor, so they arrive one after another.
- The rim is a ::before radial gradient at the cursor, masked to a 1px ring with a content-box exclude mask, so only the edge nearest the pointer lights.
Accessibility: A real <button> with a visible label; decoration is aria-hidden. Focus outline. Under prefers-reduced-motion the lights hold their positions.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
A frosted glass pane floating over drifting soft lights. Move the cursor around it and the lights behind the glass gather toward the pointer one after another, while a rim light slides round the pane's edge to face it.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- hover, mouse tracking, glow, ambient
- Techniques
- filter, raf, mouse tracking, radial gradient, pseudo elements
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A real <button> with a visible label; decoration is aria-hidden. Focus outline. Under prefers-reduced-motion the lights hold their positions.