paint-brush
How To Make a CSS Game Without Using JavaScript [Step-by-Step Guide]by@uryelah
630 reads
630 reads

How To Make a CSS Game Without Using JavaScript [Step-by-Step Guide]

by sarah chamorroOctober 26th, 2019
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

CSS isn’t the first language you think of when making games for the web, it’s just a styling language. The game we will implement in CSS is the classic Tic-tac-toe, the one you used to play in the blackboard, on sand, and that literally everyone over the age of 9 knows the trick for winning or making it a draw. 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.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail

Coin Mentioned

Mention Thumbnail
featured image - How To Make a CSS Game Without Using JavaScript [Step-by-Step Guide]
sarah chamorro HackerNoon profile picture

CSS, or Cascading Style Sheets, might not be the first language you think of when making games for the web. Heck! It isn’t even a Turing complete
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.

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!

But how? You may ask. There’s only so much time one can spend on the
browser’s inspector spamming

“!important”
and
“margin: 0 auto”
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.

How to find a good challenge for CSS though? It’s not like there are any, or any interesting, CSS challenge sites around like codewars has for dozen of programming languages.

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 Tic-tac-toe, the one you used to play in the blackboard, school books, on sand, and that literally 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 CSS-Tricks Pure CSS connect 4 article, so make sure to take a look there too if you want more from what we are about to make.

The rules:

Tic-tac-toe , noughts and crosses, or Xs and Os is a paper-and-pencil game for two players, X and O, 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. 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

“use JQuery”
Stack Overflow answer, I assure you we can do all the game logic with CSS and a couple of really useful HTML tags!

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

<input type=”checkbox”>
offers us just that.

Combining it with the CSS

:checked
pseudo class we can detect if our input is checked or not, with
:not(:checked)
, and use these states to represent our house being full or empty.

HTML:

<input id="x" type="checkbox">

CSS:

#x:checked {
    do something
}

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

input:checked
together with a selector to
select the label itself.

#x:checked + .x {
    background-color: red;
}

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?

  1. An empty state
  2. A filled with “O” state
  3. 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.

#x:checked ~ .x {
   background-color: red;
}

#o:checked ~ .o {
   background-color: yellow;
}

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

“radio”
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.

<input type="radio" name="do" id="x"/>
<input type="radio" name="do" id="o"/>

The

“radio” type
input just helped us solving the problem of having two overlapping states. But now we have another one.

With the “radio” input type we can never uncheck all the inputs that share
the same name. They are not “uncheckable” like with the

“checkbox” type
, 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.

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.

<form>
    ...
    <button type="reset">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>
#o:checked ~ .x .o {
   background-color: yellow;
}

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

:indeterminate
pseudo-class.

From the MDN web docs:

The :indeterminate CSS pseudo-class represents any form element whose state is indeterminate. (…)
<input type="radio"> elements, when all radio buttons with the same name value in the form are unchecked

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

:not(:checked)
, that would
check ONLY if the specific input is 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:

[name="do"]:indeterminate ~ label {
  background-color: black;
}

That way we have the opportunity to flex our CSS selector game and use the cool attribute selector, in the format [attribute~=value]. Neat!

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!

CSS counters 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: 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 calc() function, maybe using the 0 and 1 to set the opacity, right? …right?

The counter() CSS function returns a string representing the current value of the named counter, if there is one. It is generally used with pseudo-elements, but can be used, theoretically, anywhere a <string> value is supported.(…)
Note: The counter() function can be used with any CSS property, but support for properties other than content is experimental, and support for the type-or-unit parameter is sparse. src: 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,

:before
or
:after
, so let’s do just that. In the
example below we will increment the counter by 10 whenever the “X” input
is checked, and then we’ll assign it’s string value as the “x” label
:before
pseudo-element content.

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.

form {
  counter-reset: mark 0;
}
...
#o:checked {
  counter-increment: mark 10;
}

.x:before {
  content: counter(mark);
  color: white;
}


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

:before content
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”.

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.

#o:checked {
    counter-increment: mark 200000;
}

.x:before {
  content: counter(mark);
   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

:before
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.

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

:checked
. Since we have the sliding door
working now we can add the colors directly to them.

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.

.x {
  ...
  background-color: red;
}

.o {
  ...
  background-color: yellow;
}

...

[name="do"]:indeterminate ~ label, [name="do"]:indeterminate ~ label label {
  background-color: white;
}

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

form display to flex
, change it’s
width and give the outer “x” labels a
flex-basis of 33%
so that we can
see our houses in a
3X3 grid
like in the actual Tic-tac-toe game.

form {
  counter-reset: mark 2;
  position: relative;
  display: flex;
  margin: 30vh auto 0vh auto;
  border-radius: 20px;
  width: 300px;
  height: 300px;
  flex-flow: row wrap;
}

...

.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,

‘for=”o”
’ or
‘for=”x”
’, at the same inputs we get this identical
behavior.

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

:indeterminate
pseudo-class in mind? Now it’s the time it will be useful again for us.

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.

[name*="do"]:indeterminate + label:before {
   content: counter(mark);
   font-size: 40px;
   left: 0px;
   margin-left: -21px;
   color: transparent;
}

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.

#o:checked {
    counter-increment: mark 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)
.

input:checked:nth-of-type(even) {
    counter-increment: mark 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

:nth-of-type(odd)
. That way we can keep the switch behavior even when we have more than one input of each type.

input:checked:nth-of-type(even) {
    counter-increment: mark 200000;
}

input:checked:nth-of-type(odd) {
    counter-increment: mark -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 “id” and “for” attributes.

[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.

[name*="do"]:indeterminate + label label, [name*="do"]:indeterminate + label {
  background-color: white;
}

With this selector we get all the inner and outer labels preceded an

:indeterminate
input and give them a background color of white, like in
an empty grid cell.

input {
  position: absolute;
  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 none their pointer-events, that way they can’t get in the way of clicking the labels.

Check the update CSS style in the pen below:

Check the pen here

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.

.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.

[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: none;
}

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.

Check the full code here

There we go! Rejoice! We now have a working pure CSS game, with no JavaScript whatsoever!

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 which player won? Maybe use the counter to also make a timer or count the number of turns? Or adapt what we used here to make a whole different CSS game?!

Have fun with your CSS challenges and check some of the amazing experiments made with similar and new techniques to get your creative juices flowing.