Checkbox and radio button hacks are the (in)famous trick for creating games using just CSS. But it turns out that other elements based on user input can be hacked and gamified. There are very cool examples of developers getting creative with CSS games based on the :hover
pseudo-class, and even other games based on the :valid
pseudo-class.
What I’ve found, though, is that the :target
pseudo-class seems relatively unexplored territory in this area of CSS hacking. It’s an underrated powerful CSS feature when you think about it: :target
allows us to style anything based on the selected jump link, so we have a primitive version of client-side routing built into the browser! Let’s go mad scientist with it and see where that takes us.
Unbeatable AI in CSS
Did I type those words together? Are we going to hack CSS so hard that we hit the singularity? Try to beat the stylesheet below at Tic Tac Toe and decide for yourself.
The stylesheet will sometimes allow the game to end in a draw, so you at least have a smidge of hope.
No need to worry! CSS hasn’t gone Skynet on us yet. Like any CSS hack, the rule of thumb to determine whether a game is possible to implement with CSS is the number of possible game states. I learned that when I was able to create a 4×4 Sudoku solver but found a 9×9 version pretty darn near impossible. That’s because CSS hacks come down to hiding and showing game states based on selectors that respond to user input.
Tic Tac Toe has 5,478 legal states reachable if X moves first and there’s a famous algorithm that can calculate the optimal move for any legal state. It stands to reason, then, that we can hack together the Tic Tac Toe game completely in CSS.
OK, but how?
In a way, we are not hacking CSS at all, but rather using CSS as the Lord Almighty intended: to hide, show, and animate stuff. The “intelligence” is how the HTML is generated. It’s like a “choose your own adventure” book of every possible state in the Tic Tac Toe multiverse with the empty squares linked to the optimal next move for the computer.
We generate this using a mutant version of the minimax algorithm implemented in Ruby. And did you know that since CodePen supports HAML (which supports Ruby blocks), we can use it secretly as a Ruby playground? Now you do.
Each state our HAML generates looks like this in HTML:
<div class="b" id="--OOX----">
<svg class="o s">
<circle></circle>
</svg>
<a class="s" href="#OXOOX----">
<div></div>
</a>
<svg class="o s">
<circle class="c"></circle>
</svg>
<svg class="o s">
<circle class="c"></circle>
</svg>
<div class="x"></div>
<a class="s" href="#O-OOXX---">
<div></div>
</a>
<a class="s" href="#O-OOX-X--">
<div></div>
</a>
<a class="s" href="#O-OOX--X-">
<div></div>
</a>
<a class="s" href="#O-OOX---X">
<div></div>
</a>
</div>
With a sprinkling of surprisingly straightforward CSS, we will display only the currently selected game state using :target
selectors. We’ll also add a .c class to historical computer moves — that way, we only trigger the handwriting animation for the computer’s latest move. This gives the illusion that we are only playing on a single gameboard when we are, in reality, jumping between different sections of the document.
/* Game's parent container */
.b, body:has(:target) #--------- {
/* Game states */
.s {
display: none;
}
}
/* Game pieces with :target, elements with href */
:target, #--------- {
width: 300px;
height: 300px; /
left: calc(50vw - 150px);
top: calc(50vh - 150px);
background-image: url(/path/to/animated/grid.gif);
background-repeat: no-repeat;
background-size: 100% auto;
/* Display that game state and bring it to the forefront */
.s {
z-index: 1;
display: inline-block;
}
/* The player's move */
.x {
z-index: 1;
display: inline-block;
background-image: url("data:image/svg+xml [...]"); /** shortened for brevity **/
height: 100px;
width: 100px;
}
/* The browser's move */
circle {
animation-fill-mode: forwards;
animation-name: draw;
animation-duration: 1s;
/* Only animate the browser's latest turn */
&.c {
animation-play-state: paused;
animation-delay: -1s;
}
}
}
When a jump link is selected by clicking an empty square, the :target
pseudo-class displays the updated game state(.s), styled so that the computer’s precalculated response makes an animated entrance (.c).
Note the special case when we start the game: We need to display the initial empty grid before the user selects any jump link. There is nothing to style with :target
at the start, so we hide the initial state — with the:body:has(:target) #---------
selector — once a jump link is selected. Similarly, if you create your experiments using :target
you’ll want to present an initial view before the user begins interacting with your page.
Wrapping up
I won’t go into “why” we’d want to implement this in CSS instead of what might be an “easier” path with JavaScript. It’s simply fun and educational to push the boundaries of CSS. We could, for example, pull this off with the classic checkbox hack — someone did, in fact.
Is there anything interesting about using :target
instead? I think so because:
- We can save games in CSS! Bookmark the URL and come back to it anytime in the state you left it.
- There’s a potential to use the browser’s Back and Forward buttons as game controls. It’s possible to undo a move by going Back in time or replay a move by navigating Forward. Imagine combining
:target
with the checkbox hack to create games with a time-travel mechanic in the tradition of Braid. - Share your game states. There’s the potential of Wordle-like bragging rights. If you manage to pull off a win or a draw against the unbeatable CSS Tic Tac Toe algorithm, you could show your achievement off to the world by sharing the URL.
- It’s completely semantic HTML. The checkbox hack requires you to hide checkboxes or radio buttons, so it will always be a bit of a hack and painful horse-trading when it comes to accessibility. This approach arguably isn’t a hack since all we are doing is using jump links and divs and their styling. This may even make it — dare I say —“easier” to provide a more accessible experience. That’s not to say this is accessible right out of the box, though.
You can beat the game by starting with the bottom left cell
haha OMG you’re right. technically I guess it’s still unbeatable because it just cheats and resets if you beat it as I didn’t render the cases where O wins. the only way to win is not to play
And bottom middle, right middle and top right
You can also beat the game by starting with top right then bottom left.
Yeah, it’s fairly easy to win the game every time. It seems the “AI” can’t defend against certain types of plays.
You can beat the game every single time if you start in any corner, it literally answers with every possible bad move and loses every time
If X starts with a corner play, the O should make a move in the center square to block. If O makes a move in either of the opposite corners, they are done for.
This is really cool though and I’m sure the logic can be augmented to make this unbeatable.
Hey, I made a little game using this approach just a few months ago! It’s a little parody of the console classic Nethack. I used JavaScript to generate all possible game states for a given level, and then serialize those states to HTML and
#target
CSSthanks Andrew, cool to see a different game using the same technique!
This is interesting!
thanks for the feedback Olayinka. some of the stuff that’s baked into CSS seems too good to be true and it’s a lot of fun to experiment with
If you assign numbers to the grid, 1 through 9 such that
1 2 3
4 5 6
7 8 9
X in 8 / AI places O in 1
X in 6 / AI places O in 5
X in 9 / AI places O in 3
X in 7 = X wins predictably
Very cool! I forgot this exists, I used to see demos like this many many years ago but this ancient forgotten technology was buried in history. This brings back memories of old times
Thank you! Time travelling css in more ways than one
Top left
Bottom right
Bottom left
Middle left
Win
Slightly off topic, but if it is of interest, here’s an “AI” (but not really AI) Tic Tac Toe game I made a good while ago where the computer “learns” as you play against it: https://jvnlwn.github.io/tictactoe/
that is cool. your wordle solver looks cool too. now we just need to port it to pure CSS ;)
good content love it
thanks for the feedback Michael, glad you enjoyed the article
This is very informative blog. it really helped me a lot. Thanks for the info.
Thank you Carl