One of several CSS crimes I made for Cohost during its heyday: a version of Lights Out that functions using only CSS. Here it is, reproduced with classes instead of inline CSS. I’ve added a minor hover effect (which wasn’t possible on Cohost), but otherwise, this is identical to the original, right down to the starting state of the board.
Each button toggles itself, but also the orthogonally adjacent lights. The goal, as the name of the puzzle suggests, is to turn every light off.
Naturally, pure CSS can’t create a random setup, and this is a static page, so the initial state of the board is always the same. At the price of a droplet of JavaScript, you may use the following button:
How it works
Before I get to the CSS, notice several properties of the game itself. Since the buttons only toggle in a fixed pattern, it doesn’t matter which order buttons are pressed in. In this example, pressing A and then B will have exactly the same effect as pressing B and then A, even on the light between them: pressing one button will turn it on, and then pressing the other button will turn it back off.
Similarly, pressing the same button twice will toggle the same set of lights twice, which ultimately does nothing. So each button can be considered as either “on” (toggle lights) or “off” (do not toggle lights). That doesn’t help with solving the puzzle, since there’s no way to tell which state any given button is in, but hold that thought.
In a sense, then, instead of thinking of the buttons as altering the state of the lights, you might think of the lights as reflecting something about the buttons — specifically, a light is lit if and only if an odd number of relevant buttons (the ones that affect it, i.e., itself and its neighbors) are on.
This might sound strange, so by way of example, here’s a version of the puzzle where each button clearly indicates whether it’s on or not. Feel free to play with it a bit. Observe that it’s impossible to guess whether a button is on just from the state of the puzzle, that the buttons toggle on or off when pressed, and that the lights really do act as a count of the buttons around them.
(As a bonus feature, you can always solve the puzzle by simply turning every button off. For this basic Lights Out, that’s the only solution, but that’s not necessarily true for this type of puzzle in general.)
Parity is cool!
Given these properties, only two web features are necessary to create the puzzle. First, something that can toggle, so the puzzle can remember the state of each button (and also so the player can actually click on something). The classic way to do this is to use an invisible checkbox, and you could very well use that here — except that on Cohost, I could only write inline CSS, and you need a real stylesheet to detect a checkbox’s state (with :checked
). Thankfully, Cohost launched shortly after the introduction of the <details>
element, which allows toggling the existence of arbitrary child elements.
The other requirement is something that can count. Bear in mind, again, that I could only use inline styles, so selector trickery was right out.
Hmm.
The answer is actually what inspired me to make this in the first place. Enter mix-blend-mode
, which allows for applying image-editor-esque layer modes to arbitrary elements (or backgrounds, with the similar background-blend-mode
property). There are some familiar faces available, such as multiply
and screen
, but what caught my eye was difference
.
One of the most telling properties of a blend mode is what happens when you blend a color with white, black, or itself. Difference blending just produces, well, the difference of two colors, so blending a color with itself produces black (because anything minus itself is zero), and blending a color with black leaves it unchanged (because anything minus zero is unchanged).
And with this, I can count! Or at least I can count mod 2, which is exactly what I want.
Here’s how it actually works. The game itself is a CSS grid. Each button is a relatively-positioned <details>
element whose <summary>
is just an empty click zone, made to fill the cell with position: absolute
and inset: 0
. Each button also contains five other elements, arranged in the pattern of where the lights go. They’re absolutely positioned as well, sized to the cell with width: 100%
and height: 100%
, and then shifted over to exactly sit atop neighboring cells with, say, left: calc(-100% - 1em)
. That means align the left edge to the left edge of the parent, minus 100% of the width of the parent, minus 1em (the size of the gap) — which puts it in exactly the same position as the cell to the left.
So the basic pattern is this. (You can click that central cell to toggle it.)
Of course, if two or more of these “light” regions appear in the same cell, then… nothing happens. They just draw on top of each other, and look like a single light.
That’s where difference blending comes in. With mix-blend-mode: difference
on all of the lights, then the first light in a cell (as long as the very bottom is black) appears lit. But as soon as there’s a second one, because it’s the same color, it’ll combine with the one below it to become black again. If a third one appears, it’ll combine with the black to become lit again.
Here’s a full grid with only four working buttons, but with the lights offset so you can see the blending in action. The lights on top of the gray cells also make a sort of murky pink color now, because the background is gray instead of black.
That’s pretty much the whole thing right there. The only extra trickery is to use isolation: isolate
to ensure that the lights only blend with each other and not with the page background underneath, and also apply pointer-events: none
and z-index
liberally to ensure none of the extra visual elements interfere with clicking on a cell’s <summary>
.
The extra regions at the edges are a bit of a problem, since they stick out of the puzzle, but you can either delete the outliers or be incredibly lazy (like me) and just slap overflow: hidden
on the whole board.
The final detail is to add a little depth to the cells, so they look sort of like buttons. Rather than fiddle further with the Jenga tower of elements within the board itself, I added a second board of exactly the same size (again, position: absolute
and inset: 0
, with the same grid configuration) and filled it with 25 empty divs and some fancy backgrounds and box shadows. The colors and opacity are carefully chosen to work decently regardless of whether the light underneath is lit or not, but you could probably get fancier with blend modes and whatnot. As a convenient bonus, the borders cover up any sub-pixel rounding errors or antialiasing artifacts from the stacked lights.
And that’s it. My favorite part of this scheme is that nothing in the DOM actually remembers the state of the lights, only of the buttons! The lights themselves are purely visual, a carefully choreographed artifact of the rendering. As far as the DOM is aware, there’s just a stack of boxes that happen to overlap.