Laser Etch

No dependencies
Code format
React + Tailwind
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>
    </>
  );
}

Free to use in any project, commercial included. MIT-0, no attribution required. Licence

Install with shadcn

npx shadcn@latest add @movin/button-laser-etch
Source on GitHub

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.

More buttons

See all
Launch
Liquid Chrome
Moire
Morse