Tower
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>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
<label class="cb-tower">
<input class="cb-tower__input" type="checkbox"/>
<span class="cb-tower__art">
<span class="cb-tower__far" style="left:0;width:42px;height:118px" aria-hidden="true"></span>
<span class="cb-tower__far" style="right:0;width:46px;height:150px" aria-hidden="true"></span>
<span class="cb-tower__block" aria-hidden="true">
<span class="cb-tower__grid">
<span class="cb-tower__w" style="--d:105ms"></span>
<span class="cb-tower__w" style="--d:36ms"></span>
<span class="cb-tower__w" style="--d:120ms"></span>
<span class="cb-tower__w cb-tower__w--late" style="--d:136ms"></span>
<span class="cb-tower__w" style="--d:39ms"></span>
<span class="cb-tower__w" style="--d:85ms"></span>
<span class="cb-tower__w" style="--d:109ms"></span>
<span class="cb-tower__w" style="--d:86ms"></span>
<span class="cb-tower__w" style="--d:81ms"></span>
<span class="cb-tower__w" style="--d:105ms"></span>
<span class="cb-tower__w" style="--d:35ms"></span>
<span class="cb-tower__w cb-tower__w--late" style="--d:59ms"></span>
<span class="cb-tower__w cb-tower__w--late" style="--d:139ms"></span>
<span class="cb-tower__w cb-tower__w--late" style="--d:109ms"></span>
<span class="cb-tower__w" style="--d:73ms"></span>
<span class="cb-tower__w" style="--d:45ms"></span>
<span class="cb-tower__w cb-tower__w--late" style="--d:106ms"></span>
<span class="cb-tower__w cb-tower__w--late" style="--d:55ms"></span>
<span class="cb-tower__w" style="--d:41ms"></span>
<span class="cb-tower__w" style="--d:42ms"></span>
<span class="cb-tower__w" style="--d:27ms"></span>
<span class="cb-tower__w" style="--d:70ms"></span>
<span class="cb-tower__w cb-tower__w--late" style="--d:77ms"></span>
<span class="cb-tower__w" style="--d:136ms"></span>
<span class="cb-tower__w cb-tower__w--tick" style="--d:936ms"></span>
<span class="cb-tower__w cb-tower__w--late" style="--d:88ms"></span>
<span class="cb-tower__w" style="--d:85ms"></span>
<span class="cb-tower__w cb-tower__w--late" style="--d:121ms"></span>
<span class="cb-tower__w cb-tower__w--late" style="--d:29ms"></span>
<span class="cb-tower__w" style="--d:45ms"></span>
<span class="cb-tower__w" style="--d:22ms"></span>
<span class="cb-tower__w" style="--d:30ms"></span>
<span class="cb-tower__w" style="--d:91ms"></span>
<span class="cb-tower__w" style="--d:100ms"></span>
<span class="cb-tower__w" style="--d:80ms"></span>
<span class="cb-tower__w" style="--d:116ms"></span>
<span class="cb-tower__w cb-tower__w--tick" style="--d:852ms"></span>
<span class="cb-tower__w cb-tower__w--tick" style="--d:978ms"></span>
<span class="cb-tower__w" style="--d:21ms"></span>
<span class="cb-tower__w" style="--d:21ms"></span>
<span class="cb-tower__w" style="--d:13ms"></span>
<span class="cb-tower__w" style="--d:74ms"></span>
<span class="cb-tower__w" style="--d:165ms"></span>
<span class="cb-tower__w" style="--d:97ms"></span>
<span class="cb-tower__w cb-tower__w--late" style="--d:73ms"></span>
<span class="cb-tower__w" style="--d:142ms"></span>
<span class="cb-tower__w" style="--d:72ms"></span>
<span class="cb-tower__w" style="--d:98ms"></span>
<span class="cb-tower__w cb-tower__w--tick" style="--d:768ms"></span>
<span class="cb-tower__w cb-tower__w--tick" style="--d:894ms"></span>
<span class="cb-tower__w" style="--d:122ms"></span>
<span class="cb-tower__w" style="--d:92ms"></span>
<span class="cb-tower__w" style="--d:95ms"></span>
<span class="cb-tower__w cb-tower__w--late" style="--d:162ms"></span>
<span class="cb-tower__w cb-tower__w--tick" style="--d:180ms"></span>
<span class="cb-tower__w" style="--d:38ms"></span>
<span class="cb-tower__w" style="--d:42ms"></span>
<span class="cb-tower__w" style="--d:112ms"></span>
<span class="cb-tower__w" style="--d:152ms"></span>
<span class="cb-tower__w" style="--d:60ms"></span>
<span class="cb-tower__w cb-tower__w--tick" style="--d:684ms"></span>
<span class="cb-tower__w cb-tower__w--tick" style="--d:810ms"></span>
<span class="cb-tower__w cb-tower__w--late" style="--d:148ms"></span>
<span class="cb-tower__w" style="--d:3ms"></span>
<span class="cb-tower__w" style="--d:158ms"></span>
<span class="cb-tower__w" style="--d:162ms"></span>
<span class="cb-tower__w" style="--d:84ms"></span>
<span class="cb-tower__w cb-tower__w--tick" style="--d:222ms"></span>
<span class="cb-tower__w cb-tower__w--tick" style="--d:264ms"></span>
<span class="cb-tower__w" style="--d:85ms"></span>
<span class="cb-tower__w" style="--d:93ms"></span>
<span class="cb-tower__w" style="--d:121ms"></span>
<span class="cb-tower__w cb-tower__w--tick" style="--d:600ms"></span>
<span class="cb-tower__w cb-tower__w--tick" style="--d:726ms"></span>
<span class="cb-tower__w" style="--d:101ms"></span>
<span class="cb-tower__w" style="--d:100ms"></span>
<span class="cb-tower__w" style="--d:158ms"></span>
<span class="cb-tower__w" style="--d:28ms"></span>
<span class="cb-tower__w" style="--d:127ms"></span>
<span class="cb-tower__w" style="--d:30ms"></span>
<span class="cb-tower__w" style="--d:143ms"></span>
<span class="cb-tower__w cb-tower__w--tick" style="--d:306ms"></span>
<span class="cb-tower__w cb-tower__w--tick" style="--d:348ms"></span>
<span class="cb-tower__w" style="--d:103ms"></span>
<span class="cb-tower__w cb-tower__w--tick" style="--d:516ms"></span>
<span class="cb-tower__w cb-tower__w--tick" style="--d:642ms"></span>
<span class="cb-tower__w cb-tower__w--late" style="--d:16ms"></span>
<span class="cb-tower__w" style="--d:120ms"></span>
<span class="cb-tower__w" style="--d:45ms"></span>
<span class="cb-tower__w" style="--d:157ms"></span>
<span class="cb-tower__w" style="--d:107ms"></span>
<span class="cb-tower__w" style="--d:45ms"></span>
<span class="cb-tower__w cb-tower__w--late" style="--d:8ms"></span>
<span class="cb-tower__w" style="--d:163ms"></span>
<span class="cb-tower__w" style="--d:25ms"></span>
<span class="cb-tower__w cb-tower__w--tick" style="--d:390ms"></span>
<span class="cb-tower__w cb-tower__w--tick" style="--d:432ms"></span>
<span class="cb-tower__w cb-tower__w--tick" style="--d:558ms"></span>
<span class="cb-tower__w" style="--d:72ms"></span>
<span class="cb-tower__w" style="--d:5ms"></span>
<span class="cb-tower__w" style="--d:63ms"></span>
<span class="cb-tower__w" style="--d:27ms"></span>
<span class="cb-tower__w" style="--d:63ms"></span>
<span class="cb-tower__w cb-tower__w--late" style="--d:163ms"></span>
<span class="cb-tower__w" style="--d:91ms"></span>
<span class="cb-tower__w" style="--d:55ms"></span>
<span class="cb-tower__w" style="--d:76ms"></span>
<span class="cb-tower__w" style="--d:114ms"></span>
<span class="cb-tower__w cb-tower__w--late" style="--d:54ms"></span>
<span class="cb-tower__w cb-tower__w--tick" style="--d:474ms"></span>
<span class="cb-tower__w" style="--d:125ms"></span>
<span class="cb-tower__w" style="--d:165ms"></span>
<span class="cb-tower__w cb-tower__w--late" style="--d:141ms"></span>
<span class="cb-tower__w" style="--d:119ms"></span>
<span class="cb-tower__w cb-tower__w--late" style="--d:72ms"></span>
<span class="cb-tower__w cb-tower__w--late" style="--d:14ms"></span>
<span class="cb-tower__w" style="--d:127ms"></span>
<span class="cb-tower__w cb-tower__w--late" style="--d:144ms"></span>
<span class="cb-tower__w" style="--d:67ms"></span>
<span class="cb-tower__w" style="--d:43ms"></span>
<span class="cb-tower__w" style="--d:125ms"></span>
<span class="cb-tower__w" style="--d:20ms"></span>
<span class="cb-tower__w cb-tower__w--late" style="--d:37ms"></span>
<span class="cb-tower__w" style="--d:170ms"></span>
<span class="cb-tower__w" style="--d:90ms"></span>
<span class="cb-tower__w" style="--d:143ms"></span>
<span class="cb-tower__w cb-tower__w--late" style="--d:163ms"></span>
<span class="cb-tower__w" style="--d:8ms"></span>
<span class="cb-tower__w cb-tower__w--late" style="--d:168ms"></span>
<span class="cb-tower__w" style="--d:11ms"></span>
</span>
</span>
<span class="cb-tower__ground" aria-hidden="true"></span>
<span class="cb-tower__sign">
<span class="cb-tower__letters">Keep the lights on</span>
<span class="cb-tower__legs" aria-hidden="true"></span>
</span>
</span>
</label>.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(13, 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; }
}Goal: Create a "Tower" checkbox.
Visual behavior: 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.
Technique:
- The front is a 13 by 10 grid of small blocks, and a class on each says whether it belongs to the tick, is lit at rest or stays dark. The colour changes with stepped timing, so a window snaps on instead of fading.
- Each window carries its own delay in a CSS variable: the random ones go out in a scatter first, then the tick runs down its short arm and up its long one. Pure CSS.
Accessibility: 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.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Tower" checkbox.
Visual behavior: 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.
Technique:
- The front is a 13 by 10 grid of small blocks, and a class on each says whether it belongs to the tick, is lit at rest or stays dark. The colour changes with stepped timing, so a window snaps on instead of fading.
- Each window carries its own delay in a CSS variable: the random ones go out in a scatter first, then the tick runs down its short arm and up its long one. Pure CSS.
Accessibility: 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.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
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.