Foil
const css = `
@keyframes btn-foil-sweep { from { --btn-foil-x: -20%; } to { --btn-foil-x: 120%; } }
@property --btn-foil-x { syntax: '<percentage>'; inherits: true; initial-value: 30%; }
.btn-foil {
--btn-foil-x: 30%;
position: relative;
display: inline-flex;
flex-direction: column;
align-items: center;
gap: 3px;
padding: 13px 34px 12px;
border: none;
border-radius: 6px;
background: linear-gradient(light-dark(#f3f4f7, #1a1c1f), light-dark(#fbfcfd, #121417));
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035), inset 0 0 0 4px light-dark(#fbfcfd, #121417), inset 0 0 0 5px color-mix(in oklab, light-dark(#6d7179, #7d8087) 40%, transparent), 0 10px 22px -12px light-dark(rgba(0, 0, 0, 0.21), rgba(0, 0, 0, 0.7));
font: inherit;
cursor: pointer;
transition: transform 0.15s, box-shadow 0.15s;
}
.btn-foil:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 3px; animation: btn-foil-sweep 1.2s ease-in-out; }
.btn-foil__word, .btn-foil__small {
/* The foil: a pale band at --btn-foil-x across darker metal, clipped to the letters. */
background: linear-gradient(100deg, light-dark(#6d7179, #7d8087) calc(var(--btn-foil-x) - 30%), light-dark(#373a40, #f1f2f3) var(--btn-foil-x), light-dark(#6d7179, #7d8087) calc(var(--btn-foil-x) + 30%));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.btn-foil__word { font-size: 15px; font-weight: 600; letter-spacing: 0.32em; margin-right: -0.32em; text-transform: uppercase; }
.btn-foil__small { font-size: 9px; letter-spacing: 0.4em; margin-right: -0.4em; text-transform: uppercase; }
.btn-foil__rule { width: 28px; height: 1px; background: color-mix(in oklab, light-dark(#6d7179, #7d8087) 60%, transparent); }
.btn-foil:active { transform: translateY(1px); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035), inset 0 0 0 4px light-dark(#fbfcfd, #121417), inset 0 0 0 5px color-mix(in oklab, light-dark(#6d7179, #7d8087) 40%, transparent), inset 0 3px 8px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.5)); }
@media (prefers-reduced-motion: reduce) {
.btn-foil:focus-visible { animation: none; }
}
`;
export function FoilButton() {
return (
<>
<style>
{css}
</style>
<button
type="button"
className="btn-foil"
onPointerMove={(e) => {
const r = e.currentTarget.getBoundingClientRect();
e.currentTarget.style.setProperty('--btn-foil-x', `${Math.round(((e.clientX - r.left) / r.width) * 100)}%`);
}}
>
<span className="btn-foil__small" aria-hidden="true">
Est. 2026
</span>
<span className="btn-foil__word">Members</span>
<span className="btn-foil__rule" aria-hidden="true" />
</button>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initBtnFoil(el) on the mounted element instead.
<button type="button" class="btn-foil">
<span class="btn-foil__small" aria-hidden="true">Est. 2026</span>
<span class="btn-foil__word">Members</span>
<span class="btn-foil__rule" aria-hidden="true"></span>
</button>@keyframes btn-foil-sweep { from { --btn-foil-x: -20%; } to { --btn-foil-x: 120%; } }
@property --btn-foil-x { syntax: '<percentage>'; inherits: true; initial-value: 30%; }
.btn-foil {
--btn-foil-x: 30%;
position: relative;
display: inline-flex;
flex-direction: column;
align-items: center;
gap: 3px;
padding: 13px 34px 12px;
border: none;
border-radius: 6px;
background: linear-gradient(light-dark(#f3f4f7, #1a1c1f), light-dark(#fbfcfd, #121417));
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035), inset 0 0 0 4px light-dark(#fbfcfd, #121417), inset 0 0 0 5px color-mix(in oklab, light-dark(#6d7179, #7d8087) 40%, transparent), 0 10px 22px -12px light-dark(rgba(0, 0, 0, 0.21), rgba(0, 0, 0, 0.7));
font: inherit;
cursor: pointer;
transition: transform 0.15s, box-shadow 0.15s;
}
.btn-foil:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 3px; animation: btn-foil-sweep 1.2s ease-in-out; }
.btn-foil__word, .btn-foil__small {
/* The foil: a pale band at --btn-foil-x across darker metal, clipped to the letters. */
background: linear-gradient(100deg, light-dark(#6d7179, #7d8087) calc(var(--btn-foil-x) - 30%), light-dark(#373a40, #f1f2f3) var(--btn-foil-x), light-dark(#6d7179, #7d8087) calc(var(--btn-foil-x) + 30%));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.btn-foil__word { font-size: 15px; font-weight: 600; letter-spacing: 0.32em; margin-right: -0.32em; text-transform: uppercase; }
.btn-foil__small { font-size: 9px; letter-spacing: 0.4em; margin-right: -0.4em; text-transform: uppercase; }
.btn-foil__rule { width: 28px; height: 1px; background: color-mix(in oklab, light-dark(#6d7179, #7d8087) 60%, transparent); }
.btn-foil:active { transform: translateY(1px); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035), inset 0 0 0 4px light-dark(#fbfcfd, #121417), inset 0 0 0 5px color-mix(in oklab, light-dark(#6d7179, #7d8087) 40%, transparent), inset 0 3px 8px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.5)); }
@media (prefers-reduced-motion: reduce) {
.btn-foil:focus-visible { animation: none; }
}function initBtnFoil(root) {
// The pale band of the foil follows the pointer across the letters.
function onMove(e) {
var r = root.getBoundingClientRect();
root.style.setProperty('--btn-foil-x', Math.round(((e.clientX - r.left) / r.width) * 100) + '%');
}
root.addEventListener('pointermove', onMove);
return function () { root.removeEventListener('pointermove', onMove); };
}
document.querySelectorAll('.btn-foil').forEach(initBtnFoil);Goal: Create a "Foil" button.
Visual behavior: A members button with foil-stamped lettering on a deep plate: the metallic sheen follows your pointer across the letters, sweeps through on keyboard focus, and a press debosses the plate.
Technique:
- The word is filled with a linear gradient of darker metal round a pale band at --btn-foil-x, clipped to the text with background-clip: text.
- --btn-foil-x is registered with @property as a percentage, so it can transition and animate; pointer movement writes the pointer position into it, and focus sweeps it across with a keyframe.
- Pressing adds deeper inset shadows and nudges the plate down, as if stamped.
Accessibility: A real <button> with a visible label; decoration is aria-hidden. Keyboard focus runs the sheen across once, plus an outline. Under prefers-reduced-motion the focus sweep is off.
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 "Foil" button.
Visual behavior: A members button with foil-stamped lettering on a deep plate: the metallic sheen follows your pointer across the letters, sweeps through on keyboard focus, and a press debosses the plate.
Technique:
- The word is filled with a linear gradient of darker metal round a pale band at --btn-foil-x, clipped to the text with background-clip: text.
- --btn-foil-x is registered with @property as a percentage, so it can transition and animate; pointer movement writes the pointer position into it, and focus sweeps it across with a keyframe.
- Pressing adds deeper inset shadows and nudges the plate down, as if stamped.
Accessibility: A real <button> with a visible label; decoration is aria-hidden. Keyboard focus runs the sheen across once, plus an outline. Under prefers-reduced-motion the focus sweep is off.
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 members button with foil-stamped lettering on a deep plate: the metallic sheen follows your pointer across the letters, sweeps through on keyboard focus, and a press debosses the plate.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- hover, mouse tracking, gradient
- Techniques
- css property, gradient, mouse tracking, keyframes
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A real <button> with a visible label; decoration is aria-hidden. Keyboard focus runs the sheen across once, plus an outline. Under prefers-reduced-motion the focus sweep is off.