Morse

No dependencies
Code format
React + Tailwind
const css = `
  @keyframes toggle-morse-send { 0%, 100% { background: light-dark(#50535b, #c6c8cd); box-shadow: none; } 8% { background: light-dark(#373a40, #f1f2f3); box-shadow: 0 0 6px light-dark(#42454c, #e5e6e9); } 20% { background: light-dark(#50535b, #c6c8cd); box-shadow: none; } }
  .toggle-morse { position: relative; display: inline-flex; align-items: center; gap: 12px; color: light-dark(#42454c, #e5e6e9); font-size: 14px; cursor: pointer; user-select: none; }
  .toggle-morse__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
  .toggle-morse__track {
    position: relative;
    width: 112px;
    height: 30px;
    border-radius: 999px;
    background: light-dark(#f3f4f7, #1a1c1f);
    box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035);
  }
  .toggle-morse__input:focus-visible + .toggle-morse__track { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 3px; }
  .toggle-morse__word { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 2px; transition: opacity 0.2s; }
  .toggle-morse__word i { height: 4px; border-radius: 2px; background: light-dark(#6d7179, #7d8087); }
  .toggle-morse__g--dot { width: 3px; }
  .toggle-morse__g--dash { width: 8px; }
  .toggle-morse__g--gap { width: 4px; background: none; }
  .toggle-morse__word--on { opacity: 0; }
  .toggle-morse__word--on i:not(.toggle-morse__g--gap) { background: light-dark(#50535b, #c6c8cd); transform: scaleY(0); transition: transform 0.15s ease calc(var(--i) * 0.07s); }
  .toggle-morse__input:checked + .toggle-morse__track .toggle-morse__word--off { opacity: 0; }
  .toggle-morse__input:checked + .toggle-morse__track .toggle-morse__word--on { opacity: 1; }
  /* Once keyed in, the word is transmitted on a loop, one symbol after another. */
  .toggle-morse__input:checked + .toggle-morse__track .toggle-morse__word--on i:not(.toggle-morse__g--gap) {
    transform: none;
    animation: toggle-morse-send 2.4s linear calc(0.6s + var(--i) * 0.16s) infinite;
  }
  @media (prefers-reduced-motion: reduce) {
    .toggle-morse__word i { transition-duration: 0.01s !important; transition-delay: 0s !important; animation: none !important; }
  }
`;

// "ON" and "OFF" in Morse; spaces separate letters.
const ON = '--- -.';
const OFF = '--- ..-. ..-.';
const glyphs = (code: string) => [...code].map((c) => `toggle-morse__g toggle-morse__g--${c === '-' ? 'dash' : c === '.' ? 'dot' : 'gap'}`);

export function MorseToggle() {
  return (
    <>
      <style>
        {css}
      </style>
      <label className="toggle-morse">
        <input className="toggle-morse__input" type="checkbox" role="switch" />
        <span className="toggle-morse__track" aria-hidden="true">
          <span className="toggle-morse__word toggle-morse__word--off">
            {glyphs(OFF).map((g, i) => (
              <i key={i} className={g} />
            ))}
          </span>
          <span className="toggle-morse__word toggle-morse__word--on">
            {glyphs(ON).map((g, i) => (
              <i key={i} className={g} style={{ '--i': i } as React.CSSProperties} />
            ))}
          </span>
        </span>
        Beacon signal
      </label>
    </>
  );
}

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

Install with shadcn

npx shadcn@latest add @movin/toggle-morse
Source on GitHub

About this component

A signal switch that speaks Morse: off, the track spells OFF in dim dots and dashes; on, it keys in ON symbol by symbol and then transmits it on a loop.

Dependencies
None
Difficulty
Intermediate
Triggers
click, state change, delay stagger, continuous
Techniques
keyframes, css variables, transition delay stagger
Reduced motion
Respected
Theme
Light and dark (follows your page's color-scheme)

A native checkbox with role="switch" inside a <label>, so Space toggles it and screen readers announce a switch; the visible label names it and :focus-visible on the input outlines the visual. Under prefers-reduced-motion nothing transmits.

More toggles

See all
Orbit
Pinball
Portal
Prism