Tower

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

const COLS = 13;
const ROWS = 10;
// The tick, in the order the lights come on: down the short arm, up the long one.
const TICK: [number, number][] = [];
for (let t = 0; t < 4; t++) TICK.push([2 + t, 4 + t], [2 + t, 5 + t]);
for (let t = 1; t < 7; t++) TICK.push([5 + t, 7 - t], [5 + t, 8 - t]);
const ORDER = new Map(TICK.map(([c, r], i) => [`${c},${r}`, i]));
const rnd = (i: number) => {
  const v = Math.sin(i * 77.31) * 43758.5453;
  return v - Math.floor(v);
};
const WINDOWS = Array.from({ length: COLS * ROWS }, (_, i) => {
  const c = i % COLS;
  const r = Math.floor(i / COLS);
  const o = ORDER.get(`${c},${r}`);
  return {
    i,
    kind: o !== undefined ? 'tick' : rnd(i) > 0.74 ? 'late' : 'off',
    style: { '--d': o !== undefined ? `${180 + o * 42}ms` : `${Math.round(rnd(i + 50) * 170)}ms` } as CSSProperties,
  };
});

const css = `
  .cb-tower { --wall: light-dark(#42454c, #232529); --far: light-dark(#5f626a, #1a1c1f); --lit: light-dark(#fbfcfd, #f1f2f3); --dim: color-mix(in oklab, var(--lit) 12%, var(--wall)); --glow: color-mix(in oklab, var(--lit) 46%, var(--wall)); flex: none; position: relative; display: block; width: 264px; height: 226px; cursor: pointer; -webkit-user-select: none; user-select: none; }
  .cb-tower__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
  .cb-tower__art { position: absolute; inset: 0; display: block; border-radius: 12px; }
  .cb-tower__input:focus-visible + .cb-tower__art { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 2px; }
  .cb-tower__ground { position: absolute; left: 0; right: 0; bottom: 0; height: 2px; border-radius: 1px; background: light-dark(#6d7179, #7d8087); }
  .cb-tower__far { position: absolute; bottom: 2px; background: var(--far); }
  .cb-tower__block { position: absolute; left: 34px; right: 34px; top: 36px; bottom: 2px; box-sizing: border-box; padding: 10px 7px 0; background: var(--wall); box-shadow: inset 0 1px 0 color-mix(in oklab, var(--lit) 16%, transparent); }
  .cb-tower__grid { display: grid; grid-template-columns: repeat(${COLS}, 1fr); gap: 3px 3px; }
  .cb-tower__w { display: block; height: 13px; border-radius: 1px; background: var(--dim); transition: background-color 0.1s steps(2) var(--d), box-shadow 0.1s steps(2) var(--d); }
  .cb-tower__w--late { background: var(--glow); }
  .cb-tower__input:checked + .cb-tower__art .cb-tower__w--late { background: var(--dim); }
  .cb-tower__input:checked + .cb-tower__art .cb-tower__w--tick { background: var(--lit); box-shadow: 0 0 7px color-mix(in oklab, var(--lit) 62%, transparent); }
  .cb-tower__sign { position: absolute; left: 0; right: 0; top: 2px; display: flex; flex-direction: column; align-items: center; }
  .cb-tower__letters { padding: 3px 10px 4px; border: 1.5px solid light-dark(#50535b, #c6c8cd); border-radius: 3px; background: var(--wall); color: var(--lit); font-size: 13px; font-weight: 700; letter-spacing: 0.05em; line-height: 1.15; white-space: nowrap; }
  .cb-tower__legs { width: 120px; height: 11px; border-left: 2px solid light-dark(#5f626a, #a7abb2); border-right: 2px solid light-dark(#5f626a, #a7abb2); }
  @media (prefers-reduced-motion: reduce) {
    .cb-tower__w { transition: none; }
  }
`;

export function TowerCheckbox() {
  return (
    <>
      <style>
        {css}
      </style>
      <label className="cb-tower">
        <input className="cb-tower__input" type="checkbox" />
        <span className="cb-tower__art">
          <span className="cb-tower__far" style={{ left: 0, width: 42, height: 118 }} aria-hidden="true" />
          <span className="cb-tower__far" style={{ right: 0, width: 46, height: 150 }} aria-hidden="true" />
          <span className="cb-tower__block" aria-hidden="true">
            <span className="cb-tower__grid">
              {WINDOWS.map((w) => (
                <span key={w.i} className={`cb-tower__w${w.kind === 'off' ? '' : ` cb-tower__w--${w.kind}`}`} style={w.style} />
              ))}
            </span>
          </span>
          <span className="cb-tower__ground" aria-hidden="true" />
          <span className="cb-tower__sign">
            <span className="cb-tower__letters">Keep the lights on</span>
            <span className="cb-tower__legs" aria-hidden="true" />
          </span>
        </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-tower
Source on GitHub

About this component

An office block at night, the way buildings spell things in lit windows: the random lights go off and a tick comes on window by window, under a rooftop sign.

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

A native checkbox inside a <label>, so "Keep the lights on" is its name. It is the text of the rooftop sign, and the building is aria-hidden. Keyboard focus outlines the scene. Under prefers-reduced-motion the windows change at once.

More checkboxes

See all
Ballot
Bow
Bubble
Chalk