CRT Power

No dependencies
Code format
React + Tailwind
import { useState } from 'react';

const css = `
  @keyframes btn-crt-power-off {
    0% { transform: scale(1, 1); filter: brightness(1); }
    45% { transform: scale(1, 0.012); filter: brightness(3); }
    75% { transform: scale(0.02, 0.012); filter: brightness(4); }
    100% { transform: scale(0, 0); filter: brightness(1); }
  }
  @keyframes btn-crt-power-on {
    0% { transform: scale(0, 0.012); filter: brightness(4); }
    30% { transform: scale(1, 0.012); filter: brightness(3); }
    60% { transform: scale(1, 1.06); filter: brightness(1.6); }
    68% { opacity: 0.6; }
    74% { opacity: 1; }
    80% { opacity: 0.75; }
    100% { transform: scale(1, 1); filter: brightness(1); opacity: 1; }
  }
  @keyframes btn-crt-power-glow {
    0%, 70% { opacity: 0; }
    74% { opacity: 1; }
    100% { opacity: 0; }
  }
  @keyframes btn-crt-power-roll { to { background-position: 0 6px; } }
  .btn-crt-power {
    position: relative;
    width: 196px;
    height: 76px;
    padding: 6px;
    border: 1px solid light-dark(#cbced3, #393c41);
    border-radius: 20px;
    background: light-dark(#ebedf0, #232529);
    font-family: inherit;
    cursor: pointer;
    box-shadow: inset 0 2px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 10%, transparent), 0 12px 24px -12px light-dark(rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.6));
  }
  .btn-crt-power:focus-visible { outline: 2px solid light-dark(#42454c, #e5e6e9); outline-offset: 3px; }
  .btn-crt-power__tube {
    position: relative;
    display: block;
    height: 100%;
    border-radius: 14px / 22px;
    background: light-dark(#fbfcfd, #121417);
    overflow: hidden;
    box-shadow: inset 0 0 16px light-dark(rgba(0, 0, 0, 0.21), rgba(0, 0, 0, 0.7));
  }
  .btn-crt-power__picture {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    background: radial-gradient(ellipse at center, color-mix(in oklab, light-dark(#373a40, #f1f2f3) 16%, transparent), transparent 75%);
    color: light-dark(#373a40, #f1f2f3);
    font-size: 15px;
    font-weight: 600;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    text-shadow: 0 0 8px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 60%, transparent), 1px 0 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 30%, transparent);
  }
  .btn-crt-power__picture::after {
    content: '';
    position: absolute;
    inset: 0;
    background: repeating-linear-gradient(transparent 0 2px, color-mix(in oklab, light-dark(#fbfcfd, #121417) 55%, transparent) 2px 3px);
    animation: btn-crt-power-roll 0.4s linear infinite;
    pointer-events: none;
  }
  .btn-crt-power[aria-pressed='false'] .btn-crt-power__picture { animation: btn-crt-power-off 0.55s cubic-bezier(0.6, 0, 0.9, 0.5) forwards; }
  .btn-crt-power[data-booting] .btn-crt-power__picture { animation: btn-crt-power-on 0.75s cubic-bezier(0.22, 1, 0.36, 1); }
  .btn-crt-power__led {
    position: absolute;
    right: 14px;
    bottom: 11px;
    width: 4px;
    height: 4px;
    border-radius: 50%;
    background: light-dark(#cbced3, #393c41);
  }
  .btn-crt-power[aria-pressed='true'] .btn-crt-power__led { background: light-dark(#373a40, #f1f2f3); box-shadow: 0 0 6px light-dark(#373a40, #f1f2f3); }
  /* After switch-off, a faint afterglow dot lingers in the middle of the tube. */
  .btn-crt-power__afterglow {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 4px;
    height: 4px;
    margin: -2px;
    border-radius: 50%;
    background: light-dark(#373a40, #f1f2f3);
    box-shadow: 0 0 10px 3px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 40%, transparent);
    opacity: 0;
  }
  .btn-crt-power[aria-pressed='false'] .btn-crt-power__afterglow { animation: btn-crt-power-glow 2.2s ease-out forwards; }
  @media (prefers-reduced-motion: reduce) {
    .btn-crt-power__picture, .btn-crt-power__picture::after, .btn-crt-power__afterglow { animation: none !important; }
    .btn-crt-power[aria-pressed='false'] .btn-crt-power__picture { opacity: 0; }
  }
`;

export function CrtPowerButton() {
  const [on, setOn] = useState(true);
  const [booting, setBooting] = useState(false);

  return (
    <>
      <style>
        {css}
      </style>
      <button
        type="button"
        className="btn-crt-power"
        aria-pressed={on}
        aria-label="Power"
        data-booting={booting || undefined}
        onClick={() => {
          setOn((v) => !v);
          setBooting(!on);
        }}
        onAnimationEnd={() => setBooting(false)}
      >
        <span className="btn-crt-power__tube" aria-hidden="true">
          <span className="btn-crt-power__picture">Power</span>
          <span className="btn-crt-power__afterglow" />
        </span>
        <span className="btn-crt-power__led" aria-hidden="true" />
      </button>
    </>
  );
}

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

Install with shadcn

npx shadcn@latest add @movin/button-crt-power
Source on GitHub

About this component

A tiny CRT screen as a power button. A click switches it off the way old tubes did: the picture collapses to a bright line, then a dot, and an afterglow lingers. Another click powers it back up with a flicker.

Dependencies
None
Difficulty
Intermediate
Triggers
click, state change, glow
Techniques
keyframes, filter, pseudo elements, state machine
Reduced motion
Respected
Theme
Light and dark (follows your page's color-scheme)

A real <button> named "Power" with aria-pressed for on and off. Focus outline. Under prefers-reduced-motion the picture simply hides and shows.

More buttons

See all
Data Rain
Dot Matrix
Echo Stack
Fingerprint