Laser Etch
const css = `
@property --btn-laser-etch-p { syntax: '<percentage>'; inherits: true; initial-value: 0%; }
/* The beam switches off once the pass is done. */
@keyframes btn-laser-etch-beam {
0%, 92% { opacity: 1; }
100% { opacity: 0; }
}
@keyframes btn-laser-etch-spark {
from { transform: translate(0, 0) scale(1); opacity: 1; }
to { transform: translate(var(--sx), var(--sy)) scale(0.3); opacity: 0; }
}
.btn-laser-etch {
--btn-laser-etch-p: 0%;
position: relative;
display: grid;
place-items: center;
width: 200px;
height: 60px;
padding: 0;
border: 1px solid light-dark(#dbdde1, #2e3035);
border-radius: 10px;
background: light-dark(#f3f4f7, #1a1c1f);
font-family: inherit;
cursor: pointer;
transition: --btn-laser-etch-p 0.35s cubic-bezier(0.4, 0, 1, 1), border-color 0.3s ease;
}
.btn-laser-etch:hover, .btn-laser-etch:focus-visible {
--btn-laser-etch-p: 100%;
border-color: light-dark(#6d7179, #7d8087);
transition: --btn-laser-etch-p 1.1s linear, border-color 0.3s ease;
}
.btn-laser-etch:focus-visible { outline: 2px solid light-dark(#42454c, #e5e6e9); outline-offset: 3px; }
/* Both copies span the whole button, so the etch and the beam share one coordinate. */
.btn-laser-etch__text {
grid-area: 1 / 1;
justify-self: stretch;
text-align: center;
font-size: 22px;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.btn-laser-etch__text--outline {
color: transparent;
-webkit-text-stroke: 1px light-dark(#6d7179, #7d8087);
}
.btn-laser-etch__text--etched {
color: light-dark(#373a40, #f1f2f3);
clip-path: inset(0 calc(100% - var(--btn-laser-etch-p)) 0 0);
text-shadow: 0 0 8px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 40%, transparent);
}
.btn-laser-etch__beam {
position: absolute;
top: 6px;
bottom: 6px;
left: var(--btn-laser-etch-p);
width: 2px;
margin-left: -1px;
border-radius: 2px;
background: light-dark(#373a40, #f1f2f3);
box-shadow: 0 0 8px 1px light-dark(#373a40, #f1f2f3), 0 0 18px 4px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 35%, transparent);
opacity: 0;
transition: opacity 0.2s ease;
}
.btn-laser-etch:hover .btn-laser-etch__beam,
.btn-laser-etch:focus-visible .btn-laser-etch__beam { animation: btn-laser-etch-beam 1.2s linear forwards; }
.btn-laser-etch__spark {
position: absolute;
left: 0;
top: 50%;
width: 3px;
height: 3px;
border-radius: 50%;
background: light-dark(#373a40, #f1f2f3);
opacity: 0;
}
.btn-laser-etch:hover .btn-laser-etch__spark,
.btn-laser-etch:focus-visible .btn-laser-etch__spark { animation: btn-laser-etch-spark 0.45s ease-out var(--d) infinite; }
@media (prefers-reduced-motion: reduce) {
.btn-laser-etch, .btn-laser-etch:hover { transition: none; }
.btn-laser-etch__beam, .btn-laser-etch__spark { display: none; }
}
`;
const SPARKS = [
{ sx: '9px', sy: '-14px', d: '0s' },
{ sx: '-12px', sy: '6px', d: '0.12s' },
{ sx: '6px', sy: '16px', d: '0.24s' },
{ sx: '16px', sy: '-5px', d: '0.06s' },
{ sx: '-9px', sy: '12px', d: '0.31s' },
{ sx: '7px', sy: '-18px', d: '0.18s' },
];
export function LaserEtchButton() {
return (
<>
<style>
{css}
</style>
<button type="button" className="btn-laser-etch" aria-label="Engrave">
<span className="btn-laser-etch__text btn-laser-etch__text--outline" aria-hidden="true">
Engrave
</span>
<span className="btn-laser-etch__text btn-laser-etch__text--etched" aria-hidden="true">
Engrave
</span>
<span className="btn-laser-etch__beam" aria-hidden="true">
{SPARKS.map((s, i) => (
<span
key={i}
className="btn-laser-etch__spark"
style={{ '--sx': s.sx, '--sy': s.sy, '--d': s.d } as React.CSSProperties}
/>
))}
</span>
</button>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
<button type="button" class="btn-laser-etch" aria-label="Engrave">
<span class="btn-laser-etch__text btn-laser-etch__text--outline" aria-hidden="true">Engrave</span>
<span class="btn-laser-etch__text btn-laser-etch__text--etched" aria-hidden="true">Engrave</span>
<span class="btn-laser-etch__beam" aria-hidden="true">
<span class="btn-laser-etch__spark" style="--sx:9px;--sy:-14px;--d:0s"></span>
<span class="btn-laser-etch__spark" style="--sx:-12px;--sy:6px;--d:0.12s"></span>
<span class="btn-laser-etch__spark" style="--sx:6px;--sy:16px;--d:0.24s"></span>
<span class="btn-laser-etch__spark" style="--sx:16px;--sy:-5px;--d:0.06s"></span>
<span class="btn-laser-etch__spark" style="--sx:-9px;--sy:12px;--d:0.31s"></span>
<span class="btn-laser-etch__spark" style="--sx:7px;--sy:-18px;--d:0.18s"></span>
</span>
</button>@property --btn-laser-etch-p { syntax: '<percentage>'; inherits: true; initial-value: 0%; }
/* The beam switches off once the pass is done. */
@keyframes btn-laser-etch-beam {
0%, 92% { opacity: 1; }
100% { opacity: 0; }
}
@keyframes btn-laser-etch-spark {
from { transform: translate(0, 0) scale(1); opacity: 1; }
to { transform: translate(var(--sx), var(--sy)) scale(0.3); opacity: 0; }
}
.btn-laser-etch {
--btn-laser-etch-p: 0%;
position: relative;
display: grid;
place-items: center;
width: 200px;
height: 60px;
padding: 0;
border: 1px solid light-dark(#dbdde1, #2e3035);
border-radius: 10px;
background: light-dark(#f3f4f7, #1a1c1f);
font-family: inherit;
cursor: pointer;
transition: --btn-laser-etch-p 0.35s cubic-bezier(0.4, 0, 1, 1), border-color 0.3s ease;
}
.btn-laser-etch:hover, .btn-laser-etch:focus-visible {
--btn-laser-etch-p: 100%;
border-color: light-dark(#6d7179, #7d8087);
transition: --btn-laser-etch-p 1.1s linear, border-color 0.3s ease;
}
.btn-laser-etch:focus-visible { outline: 2px solid light-dark(#42454c, #e5e6e9); outline-offset: 3px; }
/* Both copies span the whole button, so the etch and the beam share one coordinate. */
.btn-laser-etch__text {
grid-area: 1 / 1;
justify-self: stretch;
text-align: center;
font-size: 22px;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.btn-laser-etch__text--outline {
color: transparent;
-webkit-text-stroke: 1px light-dark(#6d7179, #7d8087);
}
.btn-laser-etch__text--etched {
color: light-dark(#373a40, #f1f2f3);
clip-path: inset(0 calc(100% - var(--btn-laser-etch-p)) 0 0);
text-shadow: 0 0 8px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 40%, transparent);
}
.btn-laser-etch__beam {
position: absolute;
top: 6px;
bottom: 6px;
left: var(--btn-laser-etch-p);
width: 2px;
margin-left: -1px;
border-radius: 2px;
background: light-dark(#373a40, #f1f2f3);
box-shadow: 0 0 8px 1px light-dark(#373a40, #f1f2f3), 0 0 18px 4px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 35%, transparent);
opacity: 0;
transition: opacity 0.2s ease;
}
.btn-laser-etch:hover .btn-laser-etch__beam,
.btn-laser-etch:focus-visible .btn-laser-etch__beam { animation: btn-laser-etch-beam 1.2s linear forwards; }
.btn-laser-etch__spark {
position: absolute;
left: 0;
top: 50%;
width: 3px;
height: 3px;
border-radius: 50%;
background: light-dark(#373a40, #f1f2f3);
opacity: 0;
}
.btn-laser-etch:hover .btn-laser-etch__spark,
.btn-laser-etch:focus-visible .btn-laser-etch__spark { animation: btn-laser-etch-spark 0.45s ease-out var(--d) infinite; }
@media (prefers-reduced-motion: reduce) {
.btn-laser-etch, .btn-laser-etch:hover { transition: none; }
.btn-laser-etch__beam, .btn-laser-etch__spark { display: none; }
}Goal: Create a "Laser Etch" button.
Visual behavior: An outlined label waits to be engraved. On hover a laser line sweeps across the button, filling the letters in solid behind it and throwing sparks from the beam, then switches off once the pass is done.
Technique:
- Two copies of the label fill the button: one outlined with -webkit-text-stroke, one solid and glowing on top.
- A registered @property percentage drives the pass: hover transitions it from 0% to 100% over 1.1s, linear; leaving retracts it quickly.
- The solid copy is clipped with inset(0 calc(100% - p) 0 0), and the beam sits at left: p, so the etch edge and the beam always agree.
- Sparks are small dots on the beam flying out on a repeating keyframe with per-spark offsets and delays; the beam fades out at the end of its pass.
Accessibility: A real <button> named by aria-label; both visible copies are aria-hidden. Focus etches like hover. Under prefers-reduced-motion the fill is instant and there is no beam.
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 "Laser Etch" button.
Visual behavior: An outlined label waits to be engraved. On hover a laser line sweeps across the button, filling the letters in solid behind it and throwing sparks from the beam, then switches off once the pass is done.
Technique:
- Two copies of the label fill the button: one outlined with -webkit-text-stroke, one solid and glowing on top.
- A registered @property percentage drives the pass: hover transitions it from 0% to 100% over 1.1s, linear; leaving retracts it quickly.
- The solid copy is clipped with inset(0 calc(100% - p) 0 0), and the beam sits at left: p, so the etch edge and the beam always agree.
- Sparks are small dots on the beam flying out on a repeating keyframe with per-spark offsets and delays; the beam fades out at the end of its pass.
Accessibility: A real <button> named by aria-label; both visible copies are aria-hidden. Focus etches like hover. Under prefers-reduced-motion the fill is instant and there is no beam.
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
An outlined label waits to be engraved. On hover a laser line sweeps across the button, filling the letters in solid behind it and throwing sparks from the beam, then switches off once the pass is done.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- hover, fill, glow
- Techniques
- css property, clip path, keyframes, css variables
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A real <button> named by aria-label; both visible copies are aria-hidden. Focus etches like hover. Under prefers-reduced-motion the fill is instant and there is no beam.