visit
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.
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: 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 The answer is: if there are no pre-made cool challenges around, we’ll make
our own.
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.
Tic-tac-toe , noughts and crosses, or Xs and Os is a 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.
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: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…
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">
#x:checked {
do something
}
<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 #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.
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.
<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>
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;
}
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.
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<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 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>
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.
<label for="x" class="x">
<label for ="o" class="o"></label>
</label>
#o:checked ~ .x .o {
background-color: yellow;
}
Since we changed to “radio” inputs we can take advantage of the
:indeterminate
pseudo-class.From the MDN web docs:
The :indeterminate represents any form element whose state is indeterminate. (…)
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 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;
}
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…
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.
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:
Interesting… So that means we could use this counter variable to change
which label is visible in the house right?
The counter() 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.(…)
Note: 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: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 :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;
}
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?
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 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.
#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 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 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;
}
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.
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 flex-basis of 33%
so that we can 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%;
}
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.
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 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.
[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.
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.
#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.
<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…
input:checked:nth-of-type(even) {
counter-increment: mark 200000;
}
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.
Ok, so all the unchecked labels turn switching between red a yellow at each
turn and the checked ones…
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 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.
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 input {
position: absolute;
opacity: 0;
}
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.
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>
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.
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.
[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.
[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.
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;
}
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.
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.
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 ?!