Liquid Chrome
import { useRef } from 'react';
const css = `
@property --btn-liquid-chrome-a { syntax: '<angle>'; inherits: true; initial-value: 115deg; }
@keyframes btn-liquid-chrome-drift {
0%, 100% { --btn-liquid-chrome-a: 105deg; }
50% { --btn-liquid-chrome-a: 135deg; }
}
.btn-liquid-chrome {
--x: 50%;
--y: 50%;
position: relative;
padding: 2px;
border: none;
border-radius: 999px;
/* The rim: a thin band of the same metal at a steeper angle. */
background: linear-gradient(calc(var(--btn-liquid-chrome-a) + 60deg), light-dark(#42454c, #e5e6e9), light-dark(#cbced3, #393c41) 40%, light-dark(#373a40, #f1f2f3) 60%, light-dark(#6d7179, #7d8087));
font-family: inherit;
cursor: pointer;
box-shadow: 0 10px 24px -8px light-dark(rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.6)), 0 2px 4px light-dark(rgba(0, 0, 0, 0.09), rgba(0, 0, 0, 0.3));
animation: btn-liquid-chrome-drift 6s ease-in-out infinite;
transition: transform 0.25s ease;
}
.btn-liquid-chrome:active { transform: scale(0.97); }
.btn-liquid-chrome:focus-visible { outline: 2px solid light-dark(#42454c, #e5e6e9); outline-offset: 3px; }
.btn-liquid-chrome__face {
position: relative;
display: block;
padding: 0.85rem 2.2rem;
border-radius: 999px;
overflow: hidden;
/* Banded reflections, like a horizon in polished metal; the cursor slides them. */
background:
linear-gradient(var(--btn-liquid-chrome-a), light-dark(#50535b, #c6c8cd) 0%, light-dark(#373a40, #f1f2f3) 16%, light-dark(#6d7179, #7d8087) 30%, light-dark(#42454c, #e5e6e9) 44%, light-dark(#dbdde1, #2e3035) 50%, light-dark(#50535b, #c6c8cd) 58%, light-dark(#373a40, #f1f2f3) 76%, light-dark(#6d7179, #7d8087) 100%)
var(--x) var(--y) / 260% 260%;
color: light-dark(#fbfcfd, #121417);
font-size: 14px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
text-shadow: 0 1px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 60%, transparent);
transition: background-position 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
/* Specular hotspot under the cursor. */
.btn-liquid-chrome__face::after {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(circle 46px at var(--hx, 50%) var(--hy, 30%), color-mix(in oklab, light-dark(#373a40, #f1f2f3) 75%, transparent), transparent);
mix-blend-mode: screen;
opacity: 0;
transition: opacity 0.3s ease;
}
.btn-liquid-chrome:hover .btn-liquid-chrome__face::after { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
.btn-liquid-chrome { animation: none; }
.btn-liquid-chrome__face { transition: none; }
}
`;
export function LiquidChromeButton() {
const btn = useRef<HTMLButtonElement>(null);
return (
<>
<style>
{css}
</style>
<button
ref={btn}
type="button"
className="btn-liquid-chrome"
onPointerMove={(e) => {
const r = btn.current!.getBoundingClientRect();
const u = (e.clientX - r.left) / r.width;
const v = (e.clientY - r.top) / r.height;
const s = btn.current!.style;
// Reflections move AGAINST the cursor, as they would on a curved surface.
s.setProperty('--x', `${(100 - u * 100).toFixed(1)}%`);
s.setProperty('--y', `${(100 - v * 100).toFixed(1)}%`);
s.setProperty('--hx', `${(u * 100).toFixed(1)}%`);
s.setProperty('--hy', `${(v * 100).toFixed(1)}%`);
}}
onPointerLeave={() => {
const s = btn.current!.style;
s.setProperty('--x', '50%');
s.setProperty('--y', '50%');
}}
>
<span className="btn-liquid-chrome__face">Chrome</span>
</button>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initBtnLiquidChrome(el) on the mounted element instead.
<button type="button" class="btn-liquid-chrome">
<span class="btn-liquid-chrome__face">Chrome</span>
</button>@property --btn-liquid-chrome-a { syntax: '<angle>'; inherits: true; initial-value: 115deg; }
@keyframes btn-liquid-chrome-drift {
0%, 100% { --btn-liquid-chrome-a: 105deg; }
50% { --btn-liquid-chrome-a: 135deg; }
}
.btn-liquid-chrome {
--x: 50%;
--y: 50%;
position: relative;
padding: 2px;
border: none;
border-radius: 999px;
/* The rim: a thin band of the same metal at a steeper angle. */
background: linear-gradient(calc(var(--btn-liquid-chrome-a) + 60deg), light-dark(#42454c, #e5e6e9), light-dark(#cbced3, #393c41) 40%, light-dark(#373a40, #f1f2f3) 60%, light-dark(#6d7179, #7d8087));
font-family: inherit;
cursor: pointer;
box-shadow: 0 10px 24px -8px light-dark(rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.6)), 0 2px 4px light-dark(rgba(0, 0, 0, 0.09), rgba(0, 0, 0, 0.3));
animation: btn-liquid-chrome-drift 6s ease-in-out infinite;
transition: transform 0.25s ease;
}
.btn-liquid-chrome:active { transform: scale(0.97); }
.btn-liquid-chrome:focus-visible { outline: 2px solid light-dark(#42454c, #e5e6e9); outline-offset: 3px; }
.btn-liquid-chrome__face {
position: relative;
display: block;
padding: 0.85rem 2.2rem;
border-radius: 999px;
overflow: hidden;
/* Banded reflections, like a horizon in polished metal; the cursor slides them. */
background:
linear-gradient(var(--btn-liquid-chrome-a), light-dark(#50535b, #c6c8cd) 0%, light-dark(#373a40, #f1f2f3) 16%, light-dark(#6d7179, #7d8087) 30%, light-dark(#42454c, #e5e6e9) 44%, light-dark(#dbdde1, #2e3035) 50%, light-dark(#50535b, #c6c8cd) 58%, light-dark(#373a40, #f1f2f3) 76%, light-dark(#6d7179, #7d8087) 100%)
var(--x) var(--y) / 260% 260%;
color: light-dark(#fbfcfd, #121417);
font-size: 14px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
text-shadow: 0 1px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 60%, transparent);
transition: background-position 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
/* Specular hotspot under the cursor. */
.btn-liquid-chrome__face::after {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(circle 46px at var(--hx, 50%) var(--hy, 30%), color-mix(in oklab, light-dark(#373a40, #f1f2f3) 75%, transparent), transparent);
mix-blend-mode: screen;
opacity: 0;
transition: opacity 0.3s ease;
}
.btn-liquid-chrome:hover .btn-liquid-chrome__face::after { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
.btn-liquid-chrome { animation: none; }
.btn-liquid-chrome__face { transition: none; }
}function initBtnLiquidChrome(root) {
function move(e) {
var r = root.getBoundingClientRect();
var u = (e.clientX - r.left) / r.width;
var v = (e.clientY - r.top) / r.height;
// Reflections move against the cursor, as they would on a curved surface.
root.style.setProperty('--x', (100 - u * 100).toFixed(1) + '%');
root.style.setProperty('--y', (100 - v * 100).toFixed(1) + '%');
root.style.setProperty('--hx', (u * 100).toFixed(1) + '%');
root.style.setProperty('--hy', (v * 100).toFixed(1) + '%');
}
function leave() {
root.style.setProperty('--x', '50%');
root.style.setProperty('--y', '50%');
}
root.addEventListener('pointermove', move);
root.addEventListener('pointerleave', leave);
return function () {
root.removeEventListener('pointermove', move);
root.removeEventListener('pointerleave', leave);
};
}
document.querySelectorAll('.btn-liquid-chrome').forEach(initBtnLiquidChrome);Goal: Create a "Liquid Chrome" button.
Visual behavior: A polished metal pill. Its banded reflections drift slowly on their own and slide against the cursor as you move over it, the way they would on a curved surface, with a specular hotspot following the pointer.
Technique:
- The face is a many-stop linear gradient of light and dark bands (a horizon in metal) sized 260% so its position can slide; a rim is the same metal at a steeper angle on the 2px padding.
- The gradient angle is a registered @property that drifts between 105 and 135deg on a slow keyframe.
- pointermove writes --x / --y as the opposite of the cursor's position (so reflections move against it) and --hx / --hy at the cursor for the hotspot.
- The hotspot is a ::after radial gradient with mix-blend-mode: screen, shown on hover.
Accessibility: A real <button> with a visible label; decoration is aria-hidden. Focus outline. Under prefers-reduced-motion the drift and slide stop.
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 "Liquid Chrome" button.
Visual behavior: A polished metal pill. Its banded reflections drift slowly on their own and slide against the cursor as you move over it, the way they would on a curved surface, with a specular hotspot following the pointer.
Technique:
- The face is a many-stop linear gradient of light and dark bands (a horizon in metal) sized 260% so its position can slide; a rim is the same metal at a steeper angle on the 2px padding.
- The gradient angle is a registered @property that drifts between 105 and 135deg on a slow keyframe.
- pointermove writes --x / --y as the opposite of the cursor's position (so reflections move against it) and --hx / --hy at the cursor for the hotspot.
- The hotspot is a ::after radial gradient with mix-blend-mode: screen, shown on hover.
Accessibility: A real <button> with a visible label; decoration is aria-hidden. Focus outline. Under prefers-reduced-motion the drift and slide stop.
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 polished metal pill. Its banded reflections drift slowly on their own and slide against the cursor as you move over it, the way they would on a curved surface, with a specular hotspot following the pointer.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- hover, mouse tracking, gradient, glow
- Techniques
- gradient, css property, css variables, mouse tracking, mix blend mode
- 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 drift and slide stop.