Exhibit 12 of 17
Noughts and crosses
Two players take turns. CSS enforces the turns, refuses taken squares, declares the winner and lights the line. It cannot play against you.
The exhibit
How it works
Every square holds nine checkboxes, one for each move of the game. Move k’s boxes only appear once move k−1 is made, and only in empty squares, so the boxes on screen are always exactly the legal moves. Odd moves are X and even moves are O, so turns take care of themselves.
A win is three of one player’s boxes in a line, all checked: :has(.x.c1:checked):has(.x.c2:checked):has(.x.c3:checked), eight lines for each player. Once anyone has won, no move is open. Click your last move again to take it back. New game is a <button type="reset">.
The key CSS
Cut from this exhibit’s stylesheet as it is served; the full file is generated, and shown in part.
/* the move that's open: its boxes become real (focusable, clickable) */
.x-noughts .mv { display: none; }
.ttt:not(:has(.m1:checked)):not(:is(:has(.x.c1:checked):has(.x.c2:checked):has(.x.c3:checked), :has(.x.c4:checked):has(.x.c5:checked):has(.x.c6:checked), :has(.x.c7:checked):has(.x.c8:checked):has(.x.c9:checked), :has(.x.c1:checked):has(.x.c4:checked):has(.x.c7:checked), :has(.x.c2:checked):has(.x.c5:checked):has(.x.c8:checked), :has(.x.c3:checked):has(.x.c6:checked):has(.x.c9:checked), :has(.x.c1:checked):has(.x.c5:checked):has(.x.c9:checked), :has(.x.c3:checked):has(.x.c5:checked):has(.x.c7:checked), :has(.o.c1:checked):has(.o.c2:checked):has(.o.c3:checked), :has(.o.c4:checked):has(.o.c5:checked):has(.o.c6:checked), :has(.o.c7:checked):has(.o.c8:checked):has(.o.c9:checked), :has(.o.c1:checked):has(.o.c4:checked):has(.o.c7:checked), :has(.o.c2:checked):has(.o.c5:checked):has(.o.c8:checked), :has(.o.c3:checked):has(.o.c6:checked):has(.o.c9:checked), :has(.o.c1:checked):has(.o.c5:checked):has(.o.c9:checked), :has(.o.c3:checked):has(.o.c5:checked):has(.o.c7:checked))) .m1,
.ttt:has(.m1:checked):not(:has(.m2:checked)):not(:is(:has(.x.c1:checked):has(.x.c2:checked):has(.x.c3:checked), :has(.x.c4:checked):has(.x.c5:checked):has(.x.c6:checked), :has(.x.c7:checked):has(.x.c8:checked):has(.x.c9:checked), :has(.x.c1:checked):has(.x.c4:checked):has(.x.c7:checked), :has(.x.c2:checked):has(.x.c5:checked):has(.x.c8:checked), :has(.x.c3:checked):has(.x.c6:checked):has(.x.c9:checked), :has(.x.c1:checked):has(.x.c5:checked):has(.x.c9:checked), :has(.x.c3:checked):has(.x.c5:checked):has(.x.c7:checked), :has(.o.c1:checked):has(.o.c2:checked):has(.o.c3:checked), :has(.o.c4:checked):has(.o.c5:checked):has(.o.c6:checked), :has(.o.c7:checked):has(.o.c8:checked):has(.o.c9:checked), :has(.o.c1:checked):has(.o.c4:checked):has(.o.c7:checked), :has(.o.c2:checked):has(.o.c5:checked):has(.o.c8:checked), :has(.o.c3:checked):has(.o.c6:checked):has(.o.c9:checked), :has(.o.c1:checked):has(.o.c5:checked):has(.o.c9:checked), :has(.o.c3:checked):has(.o.c5:checked):has(.o.c7:checked))) .m2,
.ttt:has(.m2:checked):not(:has(.m3:checked)):not(:is(:has(.x.c1:checked):has(.x.c2:checked):has(.x.c3:checked), :has(.x.c4:checked):has(.x.c5:checked):has(.x.c6:checked), :has(.x.c7:checked):has(.x.c8:checked):has(.x.c9:checked), :has(.x.c1:checked):has(.x.c4:checked):has(.x.c7:checked), :has(.x.c2:checked):has(.x.c5:checked):has(.x.c8:checked), :has(.x.c3:checked):has(.x.c6:checked):has(.x.c9:checked), :has(.x.c1:checked):has(.x.c5:checked):has(.x.c9:checked), :has(.x.c3:checked):has(.x.c5:checked):has(.x.c7:checked), :has(.o.c1:checked):has(.o.c2:checked):has(.o.c3:checked), :has(.o.c4:checked):has(.o.c5:checked):has(.o.c6:checked), :has(.o.c7:checked):has(.o.c8:checked):has(.o.c9:checked), :has(.o.c1:checked):has(.o.c4:checked):has(.o.c7:checked), :has(.o.c2:checked):has(.o.c5:checked):has(.o.c8:checked), :has(.o.c3:checked):has(.o.c6:checked):has(.o.c9:checked), :has(.o.c1:checked):has(.o.c5:checked):has(.o.c9:checked), :has(.o.c3:checked):has(.o.c5:checked):has(.o.c7:checked))) .m3,
.ttt:has(.m3:checked):not(:has(.m4:checked)):not(:is(:has(.x.c1:checked):has(.x.c2:checked):has(.x.c3:checked), :has(.x.c4:checked):has(.x.c5:checked):has(.x.c6:checked), :has(.x.c7:checked):has(.x.c8:checked):has(.x.c9:checked), :has(.x.c1:checked):has(.x.c4:checked):has(.x.c7:checked), :has(.x.c2:checked):has(.x.c5:checked):has(.x.c8:checked), :has(.x.c3:checked):has(.x.c6:checked):has(.x.c9:checked), :has(.x.c1:checked):has(.x.c5:checked):has(.x.c9:checked), :has(.x.c3:checked):has(.x.c5:checked):has(.x.c7:checked), :has(.o.c1:checked):has(.o.c2:checked):has(.o.c3:checked), :has(.o.c4:checked):has(.o.c5:checked):has(.o.c6:checked), :has(.o.c7:checked):has(.o.c8:checked):has(.o.c9:checked), :has(.o.c1:checked):has(.o.c4:checked):has(.o.c7:checked), :has(.o.c2:checked):has(.o.c5:checked):has(.o.c8:checked), :has(.o.c3:checked):has(.o.c6:checked):has(.o.c9:checked), :has(.o.c1:checked):has(.o.c5:checked):has(.o.c9:checked), :has(.o.c3:checked):has(.o.c5:checked):has(.o.c7:checked))) .m4,
.ttt:has(.m4:checked):not(:has(.m5:checked)):not(:is(:has(.x.c1:checked):has(.x.c2:checked):has(.x.c3:checked), :has(.x.c4:checked):has(.x.c5:checked):has(.x.c6:checked), :has(.x.c7:checked):has(.x.c8:checked):has(.x.c9:checked), :has(.x.c1:checked):has(.x.c4:checked):has(.x.c7:checked), :has(.x.c2:checked):has(.x.c5:checked):has(.x.c8:checked), :has(.x.c3:checked):has(.x.c6:checked):has(.x.c9:checked), :has(.x.c1:checked):has(.x.c5:checked):has(.x.c9:checked), :has(.x.c3:checked):has(.x.c5:checked):has(.x.c7:checked), :has(.o.c1:checked):has(.o.c2:checked):has(.o.c3:checked), :has(.o.c4:checked):has(.o.c5:checked):has(.o.c6:checked), :has(.o.c7:checked):has(.o.c8:checked):has(.o.c9:checked), :has(.o.c1:checked):has(.o.c4:checked):has(.o.c7:checked), :has(.o.c2:checked):has(.o.c5:checked):has(.o.c8:checked), :has(.o.c3:checked):has(.o.c6:checked):has(.o.c9:checked), :has(.o.c1:checked):has(.o.c5:checked):has(.o.c9:checked), :has(.o.c3:checked):has(.o.c5:checked):has(.o.c7:checked))) .m5,
.ttt:has(.m5:checked):not(:has(.m6:checked)):not(:is(:has(.x.c1:checked):has(.x.c2:checked):has(.x.c3:checked), :has(.x.c4:checked):has(.x.c5:checked):has(.x.c6:checked), :has(.x.c7:checked):has(.x.c8:checked):has(.x.c9:checked), :has(.x.c1:checked):has(.x.c4:checked):has(.x.c7:checked), :has(.x.c2:checked):has(.x.c5:checked):has(.x.c8:checked), :has(.x.c3:checked):has(.x.c6:checked):has(.x.c9:checked), :has(.x.c1:checked):has(.x.c5:checked):has(.x.c9:checked), :has(.x.c3:checked):has(.x.c5:checked):has(.x.c7:checked), :has(.o.c1:checked):has(.o.c2:checked):has(.o.c3:checked), :has(.o.c4:checked):has(.o.c5:checked):has(.o.c6:checked), :has(.o.c7:checked):has(.o.c8:checked):has(.o.c9:checked), :has(.o.c1:checked):has(.o.c4:checked):has(.o.c7:checked), :has(.o.c2:checked):has(.o.c5:checked):has(.o.c8:checked), :has(.o.c3:checked):has(.o.c6:checked):has(.o.c9:checked), :has(.o.c1:checked):has(.o.c5:checked):has(.o.c9:checked), :has(.o.c3:checked):has(.o.c5:checked):has(.o.c7:checked))) .m6,
.ttt:has(.m6:checked):not(:has(.m7:checked)):not(:is(:has(.x.c1:checked):has(.x.c2:checked):has(.x.c3:checked), :has(.x.c4:checked):has(.x.c5:checked):has(.x.c6:checked), :has(.x.c7:checked):has(.x.c8:checked):has(.x.c9:checked), :has(.x.c1:checked):has(.x.c4:checked):has(.x.c7:checked), :has(.x.c2:checked):has(.x.c5:checked):has(.x.c8:checked), :has(.x.c3:checked):has(.x.c6:checked):has(.x.c9:checked), :has(.x.c1:checked):has(.x.c5:checked):has(.x.c9:checked), :has(.x.c3:checked):has(.x.c5:checked):has(.x.c7:checked), :has(.o.c1:checked):has(.o.c2:checked):has(.o.c3:checked), :has(.o.c4:checked):has(.o.c5:checked):has(.o.c6:checked), :has(.o.c7:checked):has(.o.c8:checked):has(.o.c9:checked), :has(.o.c1:checked):has(.o.c4:checked):has(.o.c7:checked), :has(.o.c2:checked):has(.o.c5:checked):has(.o.c8:checked), :has(.o.c3:checked):has(.o.c6:checked):has(.o.c9:checked), :has(.o.c1:checked):has(.o.c5:checked):has(.o.c9:checked), :has(.o.c3:checked):has(.o.c5:checked):has(.o.c7:checked))) .m7,
.ttt:has(.m7:checked):not(:has(.m8:checked)):not(:is(:has(.x.c1:checked):has(.x.c2:checked):has(.x.c3:checked), :has(.x.c4:checked):has(.x.c5:checked):has(.x.c6:checked), :has(.x.c7:checked):has(.x.c8:checked):has(.x.c9:checked), :has(.x.c1:checked):has(.x.c4:checked):has(.x.c7:checked), :has(.x.c2:checked):has(.x.c5:checked):has(.x.c8:checked), :has(.x.c3:checked):has(.x.c6:checked):has(.x.c9:checked), :has(.x.c1:checked):has(.x.c5:checked):has(.x.c9:checked), :has(.x.c3:checked):has(.x.c5:checked):has(.x.c7:checked), :has(.o.c1:checked):has(.o.c2:checked):has(.o.c3:checked), :has(.o.c4:checked):has(.o.c5:checked):has(.o.c6:checked), :has(.o.c7:checked):has(.o.c8:checked):has(.o.c9:checked), :has(.o.c1:checked):has(.o.c4:checked):has(.o.c7:checked), :has(.o.c2:checked):has(.o.c5:checked):has(.o.c8:checked), :has(.o.c3:checked):has(.o.c6:checked):has(.o.c9:checked), :has(.o.c1:checked):has(.o.c5:checked):has(.o.c9:checked), :has(.o.c3:checked):has(.o.c5:checked):has(.o.c7:checked))) .m8,
.ttt:has(.m8:checked):not(:has(.m9:checked)):not(:is(:has(.x.c1:checked):has(.x.c2:checked):has(.x.c3:checked), :has(.x.c4:checked):has(.x.c5:checked):has(.x.c6:checked), :has(.x.c7:checked):has(.x.c8:checked):has(.x.c9:checked), :has(.x.c1:checked):has(.x.c4:checked):has(.x.c7:checked), :has(.x.c2:checked):has(.x.c5:checked):has(.x.c8:checked), :has(.x.c3:checked):has(.x.c6:checked):has(.x.c9:checked), :has(.x.c1:checked):has(.x.c5:checked):has(.x.c9:checked), :has(.x.c3:checked):has(.x.c5:checked):has(.x.c7:checked), :has(.o.c1:checked):has(.o.c2:checked):has(.o.c3:checked), :has(.o.c4:checked):has(.o.c5:checked):has(.o.c6:checked), :has(.o.c7:checked):has(.o.c8:checked):has(.o.c9:checked), :has(.o.c1:checked):has(.o.c4:checked):has(.o.c7:checked), :has(.o.c2:checked):has(.o.c5:checked):has(.o.c8:checked), :has(.o.c3:checked):has(.o.c6:checked):has(.o.c9:checked), :has(.o.c1:checked):has(.o.c5:checked):has(.o.c9:checked), :has(.o.c3:checked):has(.o.c5:checked):has(.o.c7:checked))) .m9 { display: block; }
/* …but never in a cell that's taken */
.x-noughts .ttt:has(.c1:checked) .c1:not(:checked),
.x-noughts .ttt:has(.c1:checked) .c1:not(:checked) + label,
.x-noughts .ttt:has(.c2:checked) .c2:not(:checked),
.x-noughts .ttt:has(.c2:checked) .c2:not(:checked) + label,
.x-noughts .ttt:has(.c3:checked) .c3:not(:checked),
.x-noughts .ttt:has(.c3:checked) .c3:not(:checked) + label,
.x-noughts .ttt:has(.c4:checked) .c4:not(:checked),
.x-noughts .ttt:has(.c4:checked) .c4:not(:checked) + label,
.x-noughts .ttt:has(.c5:checked) .c5:not(:checked),
.x-noughts .ttt:has(.c5:checked) .c5:not(:checked) + label,
.x-noughts .ttt:has(.c6:checked) .c6:not(:checked),
.x-noughts .ttt:has(.c6:checked) .c6:not(:checked) + label,
.x-noughts .ttt:has(.c7:checked) .c7:not(:checked),
.x-noughts .ttt:has(.c7:checked) .c7:not(:checked) + label,
.x-noughts .ttt:has(.c8:checked) .c8:not(:checked),
.x-noughts .ttt:has(.c8:checked) .c8:not(:checked) + label,
.x-noughts .ttt:has(.c9:checked) .c9:not(:checked),
.x-noughts .ttt:has(.c9:checked) .c9:not(:checked) + label { display: none !important; }
/* a win: three of one player's boxes in a line, all checked */
.x-noughts .ttt:is(:has(.x.c1:checked):has(.x.c2:checked):has(.x.c3:checked),
:has(.x.c4:checked):has(.x.c5:checked):has(.x.c6:checked),
:has(.x.c7:checked):has(.x.c8:checked):has(.x.c9:checked),
:has(.x.c1:checked):has(.x.c4:checked):has(.x.c7:checked),
:has(.x.c2:checked):has(.x.c5:checked):has(.x.c8:checked),
:has(.x.c3:checked):has(.x.c6:checked):has(.x.c9:checked),
:has(.x.c1:checked):has(.x.c5:checked):has(.x.c9:checked),
:has(.x.c3:checked):has(.x.c5:checked):has(.x.c7:checked)) .s-xwin { display: inline; }
Accessibility
Each open square is a checkbox named for its move (“X in the centre”), and only legal moves can be reached by Tab, so the keyboard plays it fully: Tab to a square, Space. The status line is a live region, but because its text changes by CSS, screen readers may not announce the turn or the win; a script would. And CSS can’t be the second player: an opponent needs a loop.
Browser support
:has()Works in this browser.Not in this browser: Without :has() it can’t work; the page says so instead of half-working. Checked 2026-10-14: Chromium 149 yes, Firefox 141 yes.:checked + ~Works in this browser.Not in this browser: Nothing to fall back from; every browser has had it for years. Checked 2026-10-14: Chromium 149 yes, Firefox 141 yes.
