How I Built a Pure CSS Game — No JS
The dynamic of the internet is associated most of the time with JavaScript language. But it has been quite a while since CSS has been able to provide you with some of those possibilities.
In order to test these possibilities, I have decided to create my own Pure CSS Game — Coronavirus Invaders. In this article, I will teach you how to create those things you thought you could only create with JavaScript. I will do so by demonstrating these capabilities through my pure CSS game.
Note: Some of these CSS tricks are possible with CSS, but from a perspective of semantics and accessibility, it doesn’t mean that I recommend to use them on a daily basis.

Creating Functional Main Navigation
In order to create a functional navigation, I used the upper checkbox trick.
- In this trick, I have inserted a few checkboxes at the beginning of the HTML. These checkboxes represent the primary states of the game. To hide them visually, I have styled them with
position: absolute
withtop: -40px
andopacity: 0.001
. Besides that, I gave each one of them a unique ID in order to control them, for example:id=”whoAmIPopup”
. - The visual buttons of the game are
<label>
elements with an attribute offor
. The value of thefor
attribute is connected to the unique ID attribute of the checkbox in section 1. Now, when you click on one of the<label>
elements it will check/uncheck the connected checkbox. In this way, I can target the checkbox, and save states. - In CSS, I can check with the pseudo selector-
:checked
, if a checkbox is checked. And with the sibling selector of~
I can target any sibling element (and even every child of those sibling elements) and decide if to show or hide them. - In the same way (of section 3) the Start Playing (“Save the World”) is a trigger checkbox, which
display: none
the main screen, anddisplay: block
the frame of the game.

HTML
<input class="logic-checkbox" type="checkbox" id="toggleGame"> <input class="logic-checkbox" type="checkbox" id="HowToPlayPopup"> <input class="logic-checkbox" type="checkbox" id="whoAmIPopup">
<!--game navigation--> <section class="game-menu-frame"> <ul class="game-nav-list"> <li class="game-nav-item"> <label for="toggleGame" tabindex="0">Save the World</label> </li> <li class="game-nav-item"> <label for="HowToPlayPopup" tabindex="0">How to Play</label> </li> <li class="game-nav-item"> <label for="whoAmIPopup" tabindex="0">Who Am I</label> </li> </ul> </section>
<section class="game-frame"> <!--game frame--> </section>
<section class="popup common-content" id="whoAmI"> <!-- Who Am I popup --> </section>
<section class="popup common-content" id="HowToPlay"> <!-- How to play popup --> </section>
CSS (Scss)
/* checkbox which triggering popup */ #whoAmIPopup:checked ~ #whoAmI, #HowToPlayPopup:checked ~ #HowToPlay{ display: block; }
/* checkbox of start playing - trigger by pressing "save the world" button */ #toggleGame:checked { ~ .game-menu-frame{ display: none; } ~ .game-frame{ display: block; } }
/* hide popup and game frame */ #whoAmI, #HowToPlay, #game-frame{display: none;}
Killing Coronavirus
The floating Coronavirus in this game are working very similar to the — Functional Main Navigation, but with two small tweaks:

First, every coronavirus is a floating <label>
, But instead of trigger <input type=”checkbox”>
we are triggering here <input type=”radio”>
. This way, when clicking on a virus, it will be in the status of checked. This checked status represents that the virus is dead. Because it’s a radio button input, we can change the state of the virus only once (:checked
= dead).
Second, the <input type=”radio”>
is placed inside the <label>
. This way when we click on the <label>
element, it will immediately target the <input type=”radio”>
. In this case, we don’t need to give id
attribute for the inputs and the for
attribute to the <label>
. This nested connection triggers itself.
The logic of the styles is when the <input type=”radio”>
is :checked
, the sibling <div class=”body”>
element of the corona will get opacity: 0
. In this way, I make the Coronavirus disappear from the screen.
HTML
<label class="corona-virus"> <input type="radio"> <div class="body"> ... </div> </label>
CSS (Scss)
/* kill corona */ input[type="radio"]{ opacity:0.001; /* hide radio */
/*if radio is checked hide virus*/ &:checked{ ~ .body {opacity:0;} } }
Calculating the Score Result on Run-time
After we have pressed on some viruses <label>
while playing the game, those <input type=”radio”>
got :checked
. Now, we need to count all those input[type=”radio”]
which are :checked
. We do so using CSS Counters, which exist since CSS 2.1 version.

Let’s do it step by step:
- First, we define the CSS counter with the
counter-reset
property. The value is a name that we give — it is called counter value. For it to work, it needs to be defined on HTML element which is before all the<input type=”radio”>
elements in our HTML. In our case:body{ counter-reset: corona; }
. - Now we want to count all of those
input[type=”radio”]
which are:checked
. We will create a CSS selector ofinput[type=”radio”]:checked
and we will give it thecounter-increment
property, with the same counter value we created in the first section. In our case:counter-increment: corona
. - To show the summarize of the score on the screen, we will add an HTML element, for example:
<div class=”sum”>
, which has to be after all<input type=”radio”>
elements of the HTML. - Now, what we have left to do is to print it on the screen, using the pseudo-element
::after
selector while using thecontent
property style.
The CSScontent
property can get a CSS native function calledcounter
which will get thecorona
counter value, that we have already created in section 1, and increment it on section 2.
Example:.sum::after{ content: counter(corona) “/100”;}
.
HTML
<body> <!-- define counter -->
<ul class="corona-world"> <li class="corona-location corona-virus-1-location"> <label class="corona-virus"> <input type="radio"> </label> </li> ... <!-- 100 items of viruses --> </ul>
<div class="sum"> <span class="text">Score:</span> </div> ...
</body>
CSS
body{ counter-reset: corona; }
input[type="radio"]:checked{ counter-increment: corona; }
.sum::after{ content: counter(corona) "/100"; }
Creating the Countdown Clock
In this example, I create the countdown with CSS Animation and with the pseudo-elements ::before
& ::after
. Every pseudo-element represents one of the numbers of the countdown. The ::before
pseudo-element represents the counter of every ten seconds, and the second number represents every second. This way, I can create a counter that counts from 99 to 0 seconds. I gave each one of the pseudo-elements an animation on the content
property value, that runs from 9 to 0.

They both got the same animation but with a different timeline and different amount of iterations. The ::before
is iterating one time for 100 seconds. The ::after
is iterating ten times of 10 seconds each. In this way, It looks like the counter is running from 99 to 0.
For it to work well, I used the animation-timing-function
property with a value of step-end
, which creates precise jumping of the seconds. The second important thing is to use the animation-fill-mode
property with the value of forwards
. This way, when the animation will end, it will remain at the end.
HTML
<div class="count-down"> <span class="icon">⏲️</span> <span class="count"> ::before (pseudo element) ::after (pseudo element) </span> </div>
CSS
.count-down{ .count{ &::before{ content: ""; animation:countdown 100s step-end 1 forwards; } &::after{ content: ""; animation: countdown 10s step-end 10 forwards; } } }
@keyframes countdown { 0% { content: "9" } 10% { content: "8" } 20% { content: "7" } 30% { content: "6" } 40% { content: "5" } 50% { content: "4" } 60% { content: "3" } 70% { content: "2" } 80% { content: "1" } 90%, 100% { content: "0" } }
Note: CSS step animation function isn’t supported in the Safari browser.
Creating a Visual Timer
Because of the lack of support of the regular countdown in Safari, I created another visual timer, which will show as well.

This timer has fixed width
and height
. To show the status of the timeline, I used the pseudo-element ::before
, which will update in width
with the animation property according to the time that has passed.
HTML
<div class="timer"> ::before </div>
CSS
.timer { width: 150px; height: 10px; background-color: #fff; border: solid 1px #333;
&::before{ content: ""; width: 0px; height: 10px; display: block; background-color: #ccc;
animation: timer 100s linear 0s; animation-fill-mode: forwards; } }
@keyframes timer{ 0%{width: 3%;} 85%{width: 85%; background-color: #ccc;} 90%{width: 90%; background-color: red;} 100%{width: 100%; background-color: red;} }
Schedule the Game-Over Curtain
The game-over curtain is placed inside the class=”game-frame”
with height: 0
and opacity: 0
and overflow: hidden
. The animation of the curtain updates the styles to height: 100vh
and opacity:1
.

Now, the trick here is to give the curtain animation-delay
according to the time of the game, for example: animation-delay: 100s
. This way, the curtain will appear only after 100 seconds. Besides, we will give the animation animation-fill-mode: forwards
because we are aiming that when the animation ends, it will remain at the end of the timeline.
HTML
<section class="game-frame">
...HTML of Viruses ...HTML of Countdown & Timer
<section class="game-over"> <h2 class="game-title">Game Over</h2> </section>
</section>
CSS
.game-over{ height: 0; overflow: hidden; opacity: 0;
position:fixed; z-index:20; left:0; right:0; top:0;
background-color: rgba(0, 0, 0, 0.6);
animation: curtain 0.6s ease-in; animation-delay: 100s; animation-fill-mode: forwards; }
@keyframes curtain { from{height:100vh; opacity:0;} to {height:100vh; opacity:1;} }
To Summarize
In this article, I demonstrated the most important tips & tricks for how to create pure CSS games by breaking apart most of its logic.
Final Words
I hope that in this article, I gave you some inspiration on how to create pure CSS games, and maybe even inspire you to create your own pure CSS game.
If you like this article, I would appreciate applause and sharing 🙂.
You can follow me via Twitter and Linkedin.
Besides, you can find more of my content on my website — eladsc.com.
Who Am I?
I am Elad Shechter, a Web Developer specializing in CSS & HTML design and architecture.

My Full Pure CSS Game — Coronavirus Invaders CodePen:
Get more content like this
I’m starting a new community at CssClass.com.
Signup if you find it interesting 🤓
Articles, open-source projects, and deep-dive into CSS topics.
