Steam

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

const W = 256;
const H = 176;
const TICK = 'M58 84L102 126L194 40';
const rnd = (i: number) => {
  const v = Math.sin(i * 63.7) * 43758.5453;
  return v - Math.floor(v);
};
// Street lamps and lit windows: soft blobs through the fog, crisp points through the wipe.
const LIGHTS = Array.from({ length: 34 }, (_, i) => ({ x: 8 + rnd(i) * 240, y: 44 + rnd(i + 40) * 122, r: 1.4 + rnd(i + 80) * 3.2 }));
// Condensation gathers towards the bottom of the pane.
const BEADS = Array.from({ length: 46 }, (_, i) => ({ x: 6 + rnd(i + 200) * 244, y: 70 + Math.pow(rnd(i + 300), 0.55) * 100, r: 0.6 + rnd(i + 400) * 1.5 }));
const DRIPS = [
  { d: 'M60 94C61 110 58 128 60 170', delay: '0.45s' },
  { d: 'M104 136C103 148 106 158 104 174', delay: '0.62s' },
  { d: 'M150 92C151 106 148 118 150 150', delay: '0.8s' },
];

const css = `
  .cb-steam { --night: light-dark(#373a40, #121417); --dusk: light-dark(#50535b, #232529); --bulb: light-dark(#fbfcfd, #f1f2f3); --city: light-dark(#42454c, #1a1c1f); --fog: light-dark(#ebedf0, #c6c8cd); --frame: light-dark(#6d7179, #393c41); --sill: light-dark(#5f626a, #2e3035); --sill-ink: light-dark(#fbfcfd, #f1f2f3); flex: none; position: relative; display: block; width: 256px; height: 216px; cursor: pointer; -webkit-user-select: none; user-select: none; }
  .cb-steam__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
  .cb-steam__art { position: absolute; left: 0; top: 0; display: block; width: ${W}px; height: ${H + 8}px; box-sizing: border-box; padding: 4px 4px 0; border-radius: 8px 8px 0 0; background: var(--frame); }
  .cb-steam__input:focus-visible + .cb-steam__art { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 2px; }
  .cb-steam__art svg { display: block; width: 100%; height: 100%; border-radius: 5px 5px 0 0; box-shadow: inset 0 3px 6px rgba(0, 0, 0, 0.4); }
  .cb-steam__bulb { fill: var(--bulb); }
  .cb-steam__blur { filter: blur(5px); }
  .cb-steam__bead { fill: var(--bulb); opacity: 0.34; }
  .cb-steam__wipe { fill: none; stroke-width: 22; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 1; stroke-dashoffset: 1; opacity: 0; transition: opacity 1.4s ease-in, stroke-dashoffset 0s linear 1.4s; }
  .cb-steam__input:checked + .cb-steam__art .cb-steam__wipe { stroke-dashoffset: 0; opacity: 1; transition: stroke-dashoffset 0.62s cubic-bezier(0.45, 0, 0.5, 1), opacity 0s; }
  .cb-steam__drip { fill: none; stroke-width: 3.2; stroke-linecap: round; stroke-dasharray: 1; stroke-dashoffset: 1; opacity: 0; transition: opacity 1.4s ease-in, stroke-dashoffset 0s linear 1.4s; }
  .cb-steam__input:checked + .cb-steam__art .cb-steam__drip { stroke-dashoffset: 0; opacity: 1; transition: stroke-dashoffset 1.5s cubic-bezier(0.5, 0, 0.7, 0.8) var(--d), opacity 0s; }
  .cb-steam__finger { fill: var(--bulb); opacity: 0; offset-path: path('${TICK}'); offset-distance: 0%; offset-rotate: 0deg; }
  .cb-steam__input:checked + .cb-steam__art .cb-steam__finger { animation: cb-steam-finger 0.62s cubic-bezier(0.45, 0, 0.5, 1); }
  @keyframes cb-steam-finger {
    0% { opacity: 0; offset-distance: 0%; }
    12%, 85% { opacity: 0.3; }
    100% { opacity: 0; offset-distance: 100%; }
  }
  .cb-steam__sill { position: absolute; left: -4px; right: -4px; bottom: 4px; display: grid; place-items: center; height: 30px; border-radius: 3px; background: var(--sill); color: var(--sill-ink); font-size: 13.5px; font-weight: 600; box-shadow: 0 7px 10px -5px rgba(0, 0, 0, 0.5); }
  @media (prefers-reduced-motion: reduce) {
    .cb-steam__wipe, .cb-steam__drip, .cb-steam__input:checked + .cb-steam__art .cb-steam__wipe, .cb-steam__input:checked + .cb-steam__art .cb-steam__drip { transition: none; }
    .cb-steam__input:checked + .cb-steam__art .cb-steam__finger { animation: none; }
  }
`;

function City() {
  return (
    <>
      <rect width={W} height={H} fill="var(--night)" />
      <rect y="66" width={W} height={H - 66} fill="var(--dusk)" opacity="0.55" />
      <path d={`M0 ${H}V132H30V112H58V138H84V100H112V128H138V92H160V122H192V108H220V134H${W}V${H}Z`} fill="var(--city)" />
      {LIGHTS.map((l) => (
        <circle key={`${l.x}${l.y}`} className="cb-steam__bulb" cx={l.x} cy={l.y} r={l.r} />
      ))}
    </>
  );
}

export function SteamCheckbox() {
  const uid = useId().replace(/[^a-zA-Z0-9]/g, '');
  return (
    <>
      <style>
        {css}
      </style>
      <label className="cb-steam">
        <input className="cb-steam__input" type="checkbox" />
        <span className="cb-steam__art" aria-hidden="true">
          <svg viewBox={`0 0 ${W} ${H}`}>
            <defs>
              {/* The view itself, used as paint: wherever the finger goes, its stroke shows the sharp city. */}
              <pattern id={`${uid}view`} patternUnits="userSpaceOnUse" width={W} height={H}>
                <City />
              </pattern>
              <linearGradient id={`${uid}veil`} x1="0" y1="0" x2="0" y2="1">
                <stop offset="0" stopColor="var(--fog)" stopOpacity="0.3" />
                <stop offset="1" stopColor="var(--fog)" stopOpacity="0.66" />
              </linearGradient>
            </defs>
            <g className="cb-steam__blur">
              <City />
            </g>
            <rect width={W} height={H} fill={`url(#${uid}veil)`} />
            {BEADS.map((b) => (
              <circle key={`${b.x}${b.y}`} className="cb-steam__bead" cx={b.x} cy={b.y} r={b.r} />
            ))}
            <path className="cb-steam__wipe" stroke={`url(#${uid}view)`} pathLength={1} d={TICK} />
            {DRIPS.map((drip) => (
              <path key={drip.d} className="cb-steam__drip" stroke={`url(#${uid}view)`} pathLength={1} style={{ ['--d' as string]: drip.delay }} d={drip.d} />
            ))}
            <circle className="cb-steam__finger" r="12" />
          </svg>
        </span>
        <span className="cb-steam__sill">Clear the view</span>
      </label>
    </>
  );
}

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

Install with shadcn

npx shadcn@latest add @movin/checkbox-steam
Source on GitHub

About this component

A steamed-up window with the city blurred behind it. Tick it and a fingertip wipes a tick through the fog, sharp lights showing through and drops running down; untick and the fog creeps back.

Dependencies
None
Difficulty
Advanced
Triggers
click, state change, draw, delay stagger
Techniques
stroke dashoffset, svg, keyframes, css transition, filter
Reduced motion
Respected
Theme
Light and dark (follows your page's color-scheme)

A native checkbox inside a <label>, so "Clear the view" is its name. It is the text on the sill, and the window is aria-hidden. Keyboard focus outlines the window. Under prefers-reduced-motion the tick appears at once and the fingertip never shows.

More checkboxes

See all
Strap
Stud
Tower
Ballot