CSS-Only Tic Tac Toe Challenge

Back to articles

hackvertor

Author:

Gareth Heyes

@hackvertor

Published: Thu, 10 Apr 2025 11:52:06 GMT

Updated: Thu, 10 Apr 2025 18:26:02 GMT

Isometric graphic showing a person at laptop with a tic tac toe board near the wall

In this post, I dive into one of my more absurd yet wildly entertaining experiments: building a partly functional Tic Tac Toe game using pure CSS - no JavaScript, no HTML, just stylesheets.

You can play with the game here: Almost Tic Tac Toe in pure CSS!

I love throwing strange challenges at myself, especially when I have no idea whether they're even possible. That's half the fun. This time, I was deep into scroll-driven animations and thought, what if scroll position could somehow represent game state? The idea was ridiculous, which made it perfect.

The first problem I had was I had no elements to play with, I couldn't use any HTML tags. Or could I? The browser defines certain tags even if they don't exist. This means there's a HTML, body and head tag. This is pure CSS though so I can't define attributes on the tags and all I can do is use pseudo elements and control text with the content property like this:

html:before { content: "Hello world!"; }

Cool I could create text and move it about using these elements and each element has a before/after pseudo element. So I could style those elements to create headings etc. I thought about the board itself, I could produce that just using text and use CSS variables for the state. I used letter spacing to separate the board pieces and new lines to create the rows:

white-space: pre; content: var(--choice1, "-") var(--choice2, "-") var(--choice3, "-") "\A" var( --choice4,"-") var( --choice5,"-") var(--choice6,"-") ...

Cool I could now build a board 3x3 but then the next problem was I had to change the choice to "X" or "O". This was really tricky because I can't use anchors or checkboxes (this is pure CSS). I thought about this for a while and remembered about scroll driven animations, maybe I could use the scroll state for the board states?

On the body element I assigned two animations:

body { animation-name: changeStateX, changeStateO; animation-timeline: scroll(y nearest), scroll(x nearest); }

This syncs the animation to the scrollbars both vertically and horizontally. But you need the body element to scroll right, how could I do that? Well, you can make the head element very large and change it's display to block:

head { display: block; height: 5000px; width: 5000px; }

This makes the body scroll, all we need to do now is to sync the scroll with the animations, the animations simply set a CSS variable like this:

@keyframes changeStateO { 1% { --o1: "O"; } 2% { --o2: "O"; } .... }

This changes the variable name based on the scroll position. There is an unsolved problem though, we can't choose the board position to play from :( and the choices overwrite each other. To partly solve the second problem, you can use variable fallbacks, so if one variable isn't defined it will fallback to the other, this almost works but I couldn't figure out a way to make both choices stick, once the variable was defined that became the choice and if the fallback was the other choice then it wouldn't get overwritten. The end result is you can play "X" and "O" can't overwrite it but not the other way round :(

--choice1: var(--x1, var(--o1));

So the above sets the choice variable to x if it's defined otherwise it reverts to o. I've spent way too much time on this and the CSS is still pretty simple but I thought I'd write about it because it was quite fun and maybe someone more intelligent than me can solve the two remaining problems. Anyway here is the partially working game:

Almost Tic Tac Toe in pure CSS!

Update ...

@rebane2001 made their own version which is much cooler and you can actually click different parts of the board. They use container queries and transitions change the state of the board. Check out their poc!

Back to articles