CSS, or might not be the first language you think of when making games for the web. Heck! It isn’t even a programming language by itself. As it’s name states, it’s “just” a styling language, no loops, if-else statements or any of those fancy things here. Cascading Style Sheets, Turing complete A styling language eh? Sounds easy enough to master, right? Sorry to break it to you, but no. CSS is not simple, and the many people, that can’t align vertically that darn div, would argue that it’s not easy either. So what’s the solution? Simple: GIT GUD at CSS! You may ask. There’s only so much time one can spend on the browser’s inspector spamming and all around without getting bored. The trick is the same one you use when playing video games, or when learning any programming language. To get good at something you need challenges. It’s the difficulty itself that can push you forward, if it’s intriguing enough. But how? “!important” “margin: 0 auto” How to find a good challenge for CSS though? It’s not like there are any, or any interesting, CSS challenge sites around like has for dozen of programming languages. codewars The answer is: if there are no pre-made cool challenges around, we’ll make our own. We’ll make, with CSS and HTML only, a simple game that, while quite trivial with JavaScript, is a real challenge to make with CSS only. Now, because CSS was not built with the intent of building games, it doesn’t mean it’s impossible to use it to do so, or that there are no benefits from taking on the challenge. It can be very well Turing complete when combined with HTML5 and user interaction after all. Whenever people learn a programming language one of the first projects they usually complete is a simple game project, like Tc-tac-toe. It helps you to use the syntax you learned in a practical way, it gives you a complete and interactive product to show off at the end and gets you used to solve real problems with the language, so why don’t we try that with CSS too? Let’s get ready to hop on the CSS train, hide your overflows, make sure to justify-content center and get ready to flex your selector muscles because this train has no breaks. The Game Check the complete game's code in the codepen link The game we will implement in CSS is the classic the one you used to play in the blackboard, school books, on sand, and that literally . Tic-tac-toe, everyone over the age of 9 knows the trick for winning or making it a draw We will use a lot of the techniques found in the so make sure to take a look there too if you want more from what we are about to make. CSS-Tricks Pure CSS connect 4 article, The rules: , , or is a for two players, and , who take turns marking the spaces in a 3×3 grid. The player who succeeds in placing three of their marks in a horizontal, vertical, or diagonal row is the winner. Tic-tac-toe noughts and crosses Xs and Os paper-and-pencil game X O src: Wikipedia In the game above the X’s win by filling the lower horizontal row Well that sounds quite easy, let’s try to write the specs for the game: It’s played with a 3X3 grid, or 9 houses There are two players The players play the game by marking one of the 9 houses in the grid Each player plays exclusively with either an “X” or an “O” The player’s always alternate their turns A player wins when he manages to fill 3 consecutive houses, horizontally, vertically, or diagonally, with his symbol, “X” or “O” The players must stop checking houses after one of them winning It’s possible for this game to end in a draw Huh… Ok, maybe it’s not that simple. How can we even implement all those rules in CSS? Handling turn switching? Pattern finding? States? Wait, don’t “nope” out of here! This won’t become a Stack Overflow answer, I assure you we can do all the game logic with CSS and a couple of really useful HTML tags! “use JQuery” Well, if first of all we know we need to handle states, which would be a breeze with some variables but CSS doesn’t offer them, but there is a very useful HTML element that does… The Input tag A very simplified state for each of the grid’s houses would be “marked”/”full” or “not marked”/”empty”. So let’s start with that. A common HTML tag, the offers us just that. <input type=”checkbox”> Combining it with the CSS pseudo class we can detect if our input is checked or not, with , and use these states to represent our house being full or empty. :checked :not(:checked) HTML: < = = > input id "x" type "checkbox" CSS: { do something } #x :checked To better visualize it let’s add a label after the input and point the label to the input above it like: < = = > input id "x" type "checkbox" < = = > label for "x" class "x" </ > label Now when we click the label it will trigger a check or uncheck in the input and we can use the together with a selector to select the label itself. input:checked + { : red; } #x :checked .x background-color We’ll change it’s color to red whenever the input it is pointing to is checked. Since the label is the first element in the same level that comes after the input we use the adjacent sibling selector to make this condition work and apply the style we want. “+” Check the code in this step's pen Great! We now have a single house with two different states, full and empty. But if I remember correctly in Tic-tac-toe each house has three states actually, right? An empty state A filled with “O” state A filled with “X” state Since the label is itself the controller for the input checking and it can’t point to more than one input at the same time we’ll need another checkbox and another label. If we consider that the first pair represents the “X” state of the house, the new one will represent the “O” state. < = = = /> input type "checkbox" name "do" id "x" < = = = /> input type "checkbox" name "do" id "o" < = = > label for "x" class "x" </ > label < = = > label for "o" class "o" </ > label Note that we use the same name for both inputs, we will do that because they belong to the same house in the Tic-tac-toe’s grid. We have to also make sure to always keep all the inputs at the top of the file, over the labels. We are going to be working with the adjacent sibling selector, , and the general next siblings selector, , and there are not such things like a previous sibling or parent selector in CSS so we have to keep it in that order. “+” “~” Now that the “x” label is not the next sibling under the input “X” using the adjacent sibling selector won’t work anymore. But we made sure to keep all the inputs above all the labels so we can use the general next sibling selector for both the “o” and the “x” input/label pairs. ~ { : red; } ~ { : yellow; } #x :checked .x background-color #o :checked .o background-color Now we can change two different labels independently: Test it yourself in the pen Cool! Now we have two colors, when checked the “x” input paints its label red and the “o” input its label yellow. There’s a thing though, since we are using checkbox for the type of our inputs we can have both the X and the O checked at the same time, but that’s not how the Tic-tac-toe game goes. Remember, when checked a house can only be either filled with an “X” or an “O”, or in this case, the color “RED” OR the color “YELLOW”. We need to find a way to switch between the “X” and “O”, good thing that there’s an input type in HTML that happens to behave in just the perfect way. The radio input type Since we gave our two inputs the same name we can change their type to and now only one of them will be checked. If you already checked one of them and click on the other one the previous input will become unchecked and the one you just clicked will be checked. “radio” < = = = /> input type "radio" name "do" id "x" < = = = /> input type "radio" name "do" id "o" The input just helped us solving the problem of having two overlapping states. But now we have another one. “radio” type With the “radio” input type we can never uncheck all the inputs that share the same name. They are not “uncheckable” like with the , and while that might be ok if we had completed our game already and just wished to play it once without even testing it before, we kind of need to be able to restart our inputs and test our markup and styles. “checkbox” type Enters the “reset” type button. HTML provides us with a simple solution for this. If we wrap our inputs with a form tag, really where they should have been from start anyway, and add a button with the type “reset” we can now clear all the changes made to the inputs inside the form. Meaning we can now clear the states of our inputs and consequentially of our labels. ... reset < > form < = > button type "reset" </ > button </ > form Let’s add the form and reset button together with the radio inputs: Check the code here Ok, now if we click on the first and then in the second label we can see them alternating. We now have the separated “X” and “O” state, but we are trying to represent a single house of the game here so it would make sense to have the two labels wrapped inside the same element, right? Yes, but kind of no actually. Since we are using the general sibling selector “~” we need to keep the labels, or at least one of them, on the same level as the inputs. The inputs and one of the labels need to be siblings. Alright, so since we can still select one of the labels inside the other one and that would make the house look more like only one thing, lets nest the “o” label inside the “x” label and double it’s width so that we can still see both at the same time. We’ll also need to tweak the “o” label selector accordingly. < = = > label for "x" class "x" < = = > label for "o" class "o" </ > label </ > label ~ { : yellow; } #o :checked .x .o background-color The :indeterminate pseudo-class Let’s also add a new “state” to our house. Since we changed to “radio” inputs we can take advantage of the pseudo-class. :indeterminate From the MDN web docs: The represents any form element whose state is indeterminate. (…) elements, when all radio buttons with the same name value in the form are unchecked :indeterminate CSS pseudo-class <input type="radio"> That way the :indeterminate pseudo-class will only apply a style to the element after it if none of the inputs that share the same name are checked, in contrast with something like , that would check ONLY if the specific input is checked. :not(:checked) This cool selector will come in handy when we have the nine houses in for our Tic-tac-toe game, make sure to remember it. With the :indeterminate pseudo-class we’ll be able to tell with the same selector all the houses that are “empty”, or actually, have both their inputs unchecked. But for now we’ll just use it to color the “x” label black while neither the “X” nor the “O” inputs are checked: ~ { : black; } [name="do"] :indeterminate label background-color That way we have the opportunity to flex our CSS selector game and use the cool . Neat! attribute selector, in the format [attribute~=value] Check the pen here We now have something that resembles a light switch with three distinct states! Now, let’s slow down a bit and think about how we can use the above behavior to handle the turn switching in the actual Tic-tac-toe game. The first thing that comes to mind is that we can’t see both the “X” and “O” state at the same time within the same house. Only the currently checked state should be visible. We could just make our outer “x” label the same width as the inner “o” label again, but then we would only be able to click the “o” label and one can’t play Tic-tac-toe with only “O”s. If there was only a way to simulate something like a binary switch in CSS, to increment and decrement some value and then hide or show our labels accordingly… If only there was something like a, how do you say it again, counters in CSS… Say hello to CSS counters! If we were doing this with an actual programming language we could declare a counter, increment it by 1 when it’s the “X” turn, decrement 1 for the “O” turn, and use a conditional statement to show and hide the “o” label. And, hey! Turns out CSS does have something called ! counter() function let you adjust the appearance of content based on its location in a document. For example, you can use counters to automatically number the headings in a webpage. Counters are, in essence, variables maintained by CSS whose values may be incremented by CSS rules to track how many times they’re used. source: CSS counters MDN docs Interesting… So that means we could use this counter variable to change which label is visible in the house right? We could do something like multiplying the 0 by the width inside a , maybe using the 0 and 1 to set the opacity, right? …right? calc() function The function returns a string representing the current value of the named counter, if there is one. It is generally used with , but can be used, theoretically, anywhere a value is supported.(…) counter() CSS pseudo-elements <string> The counter() function can be used with any CSS property, but support for properties other than is experimental, and support for the type-or-unit parameter is sparse. src: Note: content MDN docs Uh, oh… a string, and only inside the content property. Well, that completely dismantles the little pseudo-code we had there. But we might still find a use for the counter function, even if we can’t use its value as an integer. For now, we know that we can use the named counter value as the content for a pseudo-element, or , so let’s do just that. In the example below we will increment the counter by 10 whenever the “X” input , and then we’ll assign it’s string value as the “x” label pseudo-element content. :before :after is checked :before We name and define the initial count of the counter in the input’s parent element, the form. Increment it by 10 every time the “O” input is checked and then use the counter value as the content of the outer “x” label :before pseudo-element. { : mark ; } ... { : mark ; } { : (mark); : white; } form counter-reset 0 #o :checked counter-increment 10 .x :before content counter color Test how it works in the pen Click around, in the inner and outer labels. Can you see the counter changing? You probably noticed that already, but there’s something interesting happening there with that :before element and it’s content. The counter string we are using as the :before in the content is actually “moving” the inner “o” label as it increments and decrements. The numbers take up space as content from the “x” label and push the content after it to the right. How can we leverage that to make our desired switch behavior happen, or more actually, appear to happen? The answer is in sliding doors. The slide door approach Imagine that the outer “x” label is the frame of a sliding door. The inner “o” label is the moving part of the door. If the “o” label moves right it opens the door and you can see the back of the “x” label. If it moves left it covers over the “x” label. We know we can do that using the “x” label with the counter inside it. When we increment the counter the content string will get bigger and “slide open” the door and when not incremented the door will “close”. :before content To make the above plan work we need to make it so that the counter is incremented with a string long enough to occupy the 100px width of the “o” label and to not interfere with it when not incremented. So let’s change the increment value to a longer one and change the font size a bit too. { : mark ; } { : (mark); : ; : ; : - ; } #o :checked counter-increment 200000 .x :before content counter font-size 40px left 0px margin-left 21px We’ll also change the “x” label width back to 100px and add a negative margin-left to the ”x” label pseudo-element. This will displace the first number, or initial string, of the counter to the outside of the “x” label and not interfere with the position of the inner “o” label when the counter is not incremented. :before Let’s check out our sliding door in the pen bellow. Nice, we can see that the content change makes the inner “o” label slide from left to right as we wanted it. Let’s hide the X overflow so that we don’t see the bleeding content in the left and the “o” label when it’s pushed to the side. We’ll also go ahead and delete the selectors coloring the background of each label if their inputs are . Since we have the sliding door working now we can add the colors directly to them. :checked We’ll also make conditionals that make them red and yellow since we don’t need them anymore with the window technique. And at last, add a selector that colors both labels white when their inputs are indeterminate. { ... : red; } { ... : yellow; } ... ~ , ~ { : white; } .x background-color .o background-color [name="do"] :indeterminate label [name="do"] :indeterminate label label background-color Check the illusion we created in the pen Beautiful! We managed to make the switch from “o” to “x”, represented by the colors yellow and red, in the same house. It took only a bit of smoke and mirrors. When we play Tic-tac-to we have not one, but 9 houses in the grid though. So let’s add 8 more houses, we’ll copy and paste the same labels we already have. We’ll also change the , change it’s width and give the outer “x” labels a so that we can see our houses in a like in the actual Tic-tac-toe game. form display to flex flex-basis of 33% 3X3 grid { : mark ; : relative; : flex; : auto auto; : ; : ; : ; : row wrap; } ... { ... : ; } form counter-reset 2 position display margin 30vh 0vh border-radius 20px width 300px height 300px flex-flow .x flex 0 0 33.3% : It should work like this now We now have a nice 3X3 grid with 9 houses that alternate between “yellow” and “red” at the same time whenever we click one of them. Not too bad, now we need to make each house “remember” the color it had, or actually which label was visible, at the time it was clicked. Since we only have two inputs for now and all of the labels are pointing, ’ or ’, at the same inputs we get this identical behavior. ‘for=”o” ‘for=”x” To make each house, each combination of two inputs and two labels, independent from each other let’s make 7 new pairs of inputs with specific names and make each label pair we just added point to one of them. We’ll do it like in the HTML bellow. ... ... < = = = /> input type "radio" name "do" id "x" < = = = /> input type "radio" name "do" id "o" < = = = /> input type "radio" name "do1" id "x1" < = = = /> input type "radio" name "do1" id "o1" < = = = /> input type "radio" name "do2" id "x2" < = = = /> input type "radio" name "do2" id "o2" < = = = /> input type "radio" name "do3" id "x3" < = = > label for "x" class "x" < = = > label for "o" class "o" </ > label </ > label < = = > label for "x1" class "x" < = = > label for "o1" class "o" </ > label </ > label < = = > label for "x2" class "x" < = = > label for "o2" class "o" </ > label </ > label < = = > label for "x3" class "x do" < = = > label for "o3" class "o" </ > label </ > label So, remember I asked you to keep the pseudo-class in mind? Now it’s the time it will be useful again for us. :indeterminate We’ll add one radio input before each label pair, the new input will be from the same family, have the same name attribute, from the inputs the labels are pointing to with their “for” attribute. < = = /> input name "do" type "radio" < = = > label for "x" class "x" < = = > label for "o" class "o" </ > label </ > label These inputs will be our way to determine if the content of the label just after them, remember the “+” selector, is supposed to change with each counter increment or not. We only want the houses we did not click yet to be affected by the counter change, or turn change. For this to work we will check if these inputs are :indeterminate. + { : (mark); : ; : ; : - ; : transparent; } [name*="do"] :indeterminate label :before content counter font-size 40px left 0px margin-left 21px color If they are that means that none of the above inputs from the same family, the ones connected to the “x” label after it, is checked and like that their labels should still be affected by the turns. Let’s check the result in the pen: Alright, if we start clicking each house from the top one in the left we get a grid full of red houses that become yellow as we click them. If we start from clicking any other house nothing happens though. What exactly is happening? We have a couple of problems at play here. Right now we are still incrementing the counter only when the first “o” input is checked. { : mark ; } #o :checked counter-increment 200000 That won’t do anymore though. We have eight more of those “o” inputs, each with a different id. So we need to make sure the counter increments with all of them. Doing that will bring out a second problem though. We can’t keep adding up to the counter, that would kill the switch behavior we want to achieve. Let’s take a look at our HTML and try to find our way around this. ... < > form < = = = /> input type "radio" name "do" id "x" < = = = /> input type "radio" name "do" id "o" < = = = /> input type "radio" name "do1" id "x1" < = = = /> input type "radio" name "do1" id "o1" < = = = /> input type "radio" name "do2" id "x2" < = = = /> input type "radio" name "do2" id "o2" < = = = /> input type "radio" name "do3" id "x3" < = = = /> input type "radio" name "do3" id "o3" < = = = /> input type "radio" name "do4" id "x4" < = = = /> input type "radio" name "do4" id "o4" < = = = /> input type "radio" name "do5" id "x5" < = = = /> input type "radio" name "do5" id "o5" < = = = /> input type "radio" name "do6" id "x6" < = = = /> input type "radio" name "do6" id "o6" < = = = /> input type "radio" name "do7" id "x7" < = = = /> input type "radio" name "do7" id "o7" < = = = /> input type "radio" name "do8" id "x8" < = = = /> input type "radio" name "do8" id "o8" At first glance, we can detect a pattern we are following with the “x” and “o” inputs. We start with an “x”, then go to an “o”, and to an “x” again… Knowing this makes it easy to increment the counter in the event of any “o” input check. We can select them with . :nth-of-type(even) { : mark ; } input :checked :nth-of-type(even) counter-increment 200000 But remember when I mentioned we can’t keep adding 200000 again and again with each “o” input that gets checked? We can counter that by also decrementing the counter each time an “x” input is checked, with . That way we can keep the switch behavior even when we have more than one input of each type. :nth-of-type(odd) { : mark ; } { : mark - ; } input :checked :nth-of-type(even) counter-increment 200000 input :checked :nth-of-type(odd) counter-increment 200000 Let’s also go to the CSS and comment out the selector making the labels preceded by an indeterminate label white, that way we can visualize better the switch between the turns, or “x” and “o” labels, in the whole game grid. Check the pen here Ok, so all the unchecked labels turn switching between red a yellow at each turn and the checked ones… What the Heck? Aren’t we still just painting all the houses yellow? I know it may not look like it, but our code is working like it should be. The clicked houses are not being affected anymore by the counter and we can even see how the other ones are alternating the “x” and “o” label positions at each turn. The reason all of them turn yellow at the end, or actually, have only the “x” label visible, no matter if you clicked them when they were red, is that by making the clicked houses unaffected by the counter they go back to their initial state that is having the inner “o” label over the “x label”. We only see the red houses when the :before element of the label has an incremented counter content of “200002". We can fix this by selecting all the “x” labels pointing to checked “X” inputs, using the general sibling selector, since all of them have exclusive and attributes. “~” “id” “for” ~ + , ~ + , ~ + , ~ + , ~ + , ~ + , ~ + , ~ + , ~ + { : ; : ; : ; } [id="x"] :checked [name="do"] :not( :indeterminate) label :before [id="x1"] :checked [name="do1"] :not( :indeterminate) label :before [id="x2"] :checked [name="do2"] :not( :indeterminate) label :before [id="x3"] :checked [name="do3"] :not( :indeterminate) label :before [id="x4"] :checked [name="do4"] :not( :indeterminate) label :before [id="x5"] :checked [name="do5"] :not( :indeterminate) label :before [id="x6"] :checked [name="do6"] :not( :indeterminate) label :before [id="x7"] :checked [name="do7"] :not( :indeterminate) label :before [id="x8"] :checked [name="do8"] :not( :indeterminate) label :before font-size 40px content "200002" opacity 0 With that we can return them the correct content, manually setting it to “200002", and “keep the door open” so that we can still see the “x” label we clicked on. All the houses now behave like expected. It’s not good that we can see the turn changing happening at the un-clicked houses though. To clear this visual mess and to be able to play our Tic-tac-toe game without having to guess which house is still unchecked we’ll un-comment the CSS selector we commented out before and modify it a bit. + , + { : white; } [name*="do"] :indeterminate label label [name*="do"] :indeterminate label background-color With this selector we get all the inner and outer labels preceded an input and give them a background color of white, like in an empty grid cell. :indeterminate { : absolute; : ; } input position opacity 0 And look at that! We have a legit playable Tic-tac-toe in our hands: Check the pen here Cheers! We now have practically all of the game logic set up but… Before we can challenge someone to play a Tic-tac-toe match t̶o̶ ̶d̶e̶a̶t̶h̶ ̶t̶o̶ ̶a̶v̶e̶n̶g̶e̶ ̶y̶o̶u̶r̶ ̶f̶a̶m̶i̶l̶y̶ ̶h̶o̶n̶o̶r̶, having some visual feedback when a player wins the game would be pretty convenient. Winning cases We need to show the players when one of them fulfills the conditions to win the game and stop them from playing from there. Let’s start by adding to the HTML and CSS for each winning case. Remember, a player wins Tic-tac-toe whenever they mark an uninterrupted sequence of three houses with their symbol. The cues we’ll need are: Three for the horizontal ones, on the top, center and bottom rows of the board Three for the vertical ones, on the left, center and right columns of the board Two for the diagonal ones, one sloping to the left and another from the side Let’s set them up as divs at the end of the form so that we can select them with the general sibling selector according to the state of the inputs above them. “~” < = > div class "res ve-left" </ > div < = > div class "res ve-center" </ > div < = > div class "res ve-right" </ > div < = > div class "res ho-top" </ > div < = > div class "res ho-center" </ > div < = > div class "res ho-end" </ > div < = > div class "res di-right" </ > div < = > div class "res di-left" </ > div We’ll give each one of them blue borders, the same width of the rows/columns so that they can “wrap” around the winning sequence of marked houses, and position each over the correct area of our game grid. We’ll also set to , that way they can’t get in the way of clicking the labels. none their pointer-events Check the update CSS style in the pen below: Now that we have our 8 visual wrappers for each victory case let’s make them invisible, with an 0 opacity, and focus on the winning patterns matching. Check the pen here { ... : } .res opacity 0 Our winning wrappers should only show up again when a sequence of three houses fulfills the correct condition or pattern. So let’s take another look at our inputs are organized in our HTML and figure out how to select the patterns we are looking for. First for the horizontal patterns. We need three subsequent houses in the same row with the same type of input checked. Since we are aligning all of our houses inside a flex row container with a wrap, and we have each house represented by two inputs at the top of the form this pattern will be the easiest. We need to find a sequence of three checked/unchecked input pairs and select the correct winning wrapper under them at the end of the form. We have to take into account that we have three possible positions for the row pattern, top row, middle row, and bottom row. So, let’s apply the selector pattern three times, one for each row. + + + + ~ { : ; } + + + + ~ { : ; } + + + + ~ { : ; } [name="do"] :checked input [name="do1"] :checked input [name="do2"] :checked .ho-top opacity 1 [name="do3"] :checked input [name="do4"] :checked input [name="do5"] :checked .ho-center opacity 1 [name="do6"] :checked input [name="do7"] :checked input [name="do8"] :checked .ho-end opacity 1 For the vertical patters: We know that each house of the board is represented by two sequential inputs and that one row of three houses is comprised of 6 inputs in total. That way we know that if an input of the same type is checked under a previous one in the same column but different rows, their distance will be of five inputs. We can apply the pattern like bellow for each column. + + + + + + + + + + + + ~ { : ; } + + + + + + + + + + + + ~ { : ; } + + + + + + + + + + + + ~ { : ; } [name="do"] :checked input input input input input [name="do3"] :checked input input input input input [name="do6"] :checked .ve-left opacity 1 [name="do1"] :checked input input input input input [name="do4"] :checked input input input input input [name="do7"] :checked .ve-center opacity 1 [name="do2"] :checked input input input input input [name="do5"] :checked input input input input input [name="do8"] :checked .ve-right opacity 1 For the slope patterns: With the right slope diagonal pattern, the only difference from the vertical pattern is that the next house after each one is one more house away or to the right. So we add a house, two inputs, to the five from the vertical pattern and have a pattern of one checked input followed by seven inputs: + + + + + + + + + + + + + + + + ~ { : ; } [name="do"] :checked input input input input input input input [name="do4"] :checked input input input input input input input [name="do8"] :checked .di-right opacity 1 Lastly, for the slope left pattern we move one house closer or to the left, so now we subtract 2 inputs from 5 and have one checked followed by three unchecked inputs. + + + + + + + + ~ { : ; } [name="do2"] :checked input input input [name="do4"] :checked input input input [name="do6"] :checked .di-left opacity 1 Notice that for each of the above selections to work we have to remember to always use the general sibling selector, , at the end followed by the winning wrapper we want to make visible. “~” In the pen below the game will show a blue border around the three houses whenever the three have the same color. Check the pen here Nice, we are almost there! Now let’s fix a couple of remaining bugs. We don’t want the players to be able to keep clicking the houses after one of them won the game. Let’s get all of the selectors we made just now and use them to select all the labels and give them a pointer-events “none” value. + + + + ~ , + + + + ~ , + + + + ~ , + + + + + + + + + + + + ~ , + + + + + + + + + + + + ~ , + + + + + + + + + + + + ~ , + + + + + + + + + + + + + + + + ~ , + + + + + + + + ~ { : none; } [name="do"] :checked input [name="do1"] :checked input [name="do2"] :checked label [name="do3"] :checked input [name="do4"] :checked input [name="do5"] :checked label [name="do6"] :checked input [name="do7"] :checked input [name="do8"] :checked label [name="do"] :checked input input input input input [name="do3"] :checked input input input input input [name="do6"] :checked label [name="do1"] :checked input input input input input [name="do4"] :checked input input input input input [name="do7"] :checked label [name="do2"] :checked input input input input input [name="do5"] :checked input input input input input [name="do8"] :checked label [name="do"] :checked input input input input input input input [name="do4"] :checked input input input input input input input [name="do8"] :checked label [name="do2"] :checked input input input [name="do4"] :checked input input input [name="do6"] :checked label pointer-events Wew… that… was a very long selector. Let’s also hide the reset button during the play, we don’t want anyone “chicking out” mid-session after all. Here we play until the end! If we select the button in CSS and give it an opacity of 0, then we can use the same huge selector above and change it’s opacity to 1 at the end of the game. We’ll also use actual and ⭕ images instead of colors, that way it will be closer to the feel of the original game. ❌ There we go! Rejoice! We now have a working pure CSS game, with no JavaScript whatsoever! Check the full code here Our game is awesome, but we have to admit it’s not looking so hot. If you want you can go ahead and style it to your liking or take a pick at the finished game bellow. In the final version, I added some new background colors, box-shadows and a couple of animations to bring out its appeal. It plays like a charm! You can play and see the full code in this codepen I’m sure there’s still a lot to improve for this Tic-tac-toe HTML/CSS implementation. Go ahead and try adding or changing some features. Can you make selectors that also display ? Maybe use the counter to also or count the number of turns? Or adapt what we used here to make a whole ?! which player won make a timer different CSS game Have fun with your CSS challenges and check of the made with and techniques to get your creative juices flowing. some amazing experiments similar new