home

Pure CSS Games

1: Using CSS counters in creative ways

January 2025

Starting With Questions

Before you start, you can play a little with the CSS-only activities below. Ask yourself these questions:

If you start with questions, each time you find an answer, you get a little buzz of satisfaction. It’s like with jokes. If you hear the punchline first, you might think: “When am I ever going to need an answer like this?” If you hear the build-up first, the punchline becomes something you want to share with your friends.

These are not all complete games or puzzles. You can think of them as illustrations of the concepts that you will be reading more about below.

You might also have other questions, like:

Keep these questions in mind. You’ll find answers to them in forthcoming tutorials… unless you can create good punchlines answers for yourself first : )

What I expect you to know already

I’m assuming that you already:

Nested CSS

Nested CSS has been available in all major browsers since December 2023, and it’s available to over 80% of all users, so it should work in your development browser. It allows me to write shorter CSS code.

Here’s an example of how I use nested CSS to create numbered links:

ol {
  list-style-type: none;
  counter-reset: item 0;

  li {
    margin: 0.5em;
    border: 1px solid black;
  
    a {
      color: inherit;
      text-decoration: none;
      display: inline-block;
      padding: 0.5em;
      width: 100%;
      box-sizing: border-box;

      &::before {
        counter-increment: item 1;
        content: counter(item) ". ";
      }

      &:hover {
        text-decoration: underline;
      }
    }
  }
}

The result would look something like this:

An ordered list containing numbered links

The nested CSS above will be treated exactly as if it were written in this standard expanded format:

ol {
  list-style-type: none;
  counter-reset: item 0;
}
ol li {
  margin: 0.5em;
  border: 1px solid black;
}
ol li a {
  color: inherit;
  text-decoration: none;
  display: inline-block;
  padding: 0.5em;
  width: 100%;
  box-sizing: border-box;
}
ol li a::before {
  counter-increment: item 1;
  content: counter(item) ". ";
}
ol li a:hover {
  text-decoration: underline;
}

What you might not know yet

If you don’t yet know about the recently-introduced :has() pseudo-class, don’t worry. You’ll see plenty of examples on how to use it.

Learning To Count

The Origin Story: ordered lists

If you create an <ol> ordered list, your browser will automatically number the lines. Try it:

<h1>TO DO LIST</h1>
<ol>
  <li>Buy a tortoise</li>
  <li>Call it 'The Speed of Light'</li>
  <li>Tell people I can run faster than The Speed of Light</li>
</ol>

You can change the number that the list starts with by using the start attribute in your HTML:

<ol start="10">
  <li>Buy a tortoise</li>
  <li>Call it 'The Speed of Light'</li>
  <li>Tell people I can run faster than The Speed of Light</li>
</ol>
using <ol start="10">

This works because under the hood, the browser uses a counter called list-item. You can control this counter through CSS. With the following rule, all your <ol> elements will start from 101… even those where you set the start property in your HTML file. The counter property takes precedence…

ol {
  counter-set: list-item 101;
}
or rather: “should take precedence”

… except that since Google released Chromium 126 on 11 June 2024, this use of list-item is broken in Webkit browsers such as Google Chrome, Opera and Microsoft Edge. It still works in Firefox and Safari, so you can test it there. It’s not a big deal, because you won’t need to use exactly this feature for anything in your CSS-only games. It’s just nice to know that ordered <ol> lists were where the counter feature came from.

Free For All

Ordered lists don’t claim exclusive rights over counters. Browsers make counters available to any element. They give you full control of any kind of numbering process. Here’s what an ordered list does naturally:

<ol>
  <li>For the money</li>
  <li>For the show</li>
  <li>To get ready and go cat go</li>
</ol>
Standard numbering with an ordered list

Here’s how you could create a similar (not identical) result using a <ul> unordered list.

<ul>
  <li>For the money</li>
  <li>For the show</li>
  <li>To get ready and go cat go</li>
</ul>
ul {
  list-style-type: none;
  counter-set: item;

  li {
    counter-increment: item;
  }

  li::before {
    content: "Item " counter(item) ". ";
  }
}

Here’s what happens:

numbered unordered list
Summary

Note that you can provide several items in the value for the content attribute, like "Item " counter(item) ". ", and the CSS elves will concatenate them together for you.

Summary

Note that I use counter-set and not counter-reset, because the current version of Firefox (128.0.3) does not always handle counter-reset as you would expect it to.

Seeing How CSS Works

Warning

What I show below is not good HTML. You are not supposed to use <b> tags inside a <ul> list, and there’s not a real-life case for leaving them empty. But this makeshift HTML is good enough to show you how the CSS elves work.

<b></b>
<ul>
  <b><<b>
  <li>For the money</li>
  <b></b>
  <li>For the show</li>
  <b></b>
  <li>To get ready and go cat go</li>
  <b></b>
</ul>

In my first example, I gave no initial value for the item counter, so it started with the default value of 0. The CSS elves then found the first list item (“For the money”), and incremented the item counter by the default amount: 1. So when the first list item was shown, the content of its ::before pseudo-element was “Item 1.”

In the CSS below, I create a custom counter called item, with an initial value of 10. I also use counter-increment: item 5; each time an <li> item is treated, so the first item starts with an item value of 15.

ul {
  list-style-type: none;
  counter-set: item 10;

  li {
    counter-increment: item 5;
  }

  li::before {
    content: "Item " counter(item) ". ";
  }
}

b::before {
  content: "—[" counter(item) "]—";
}

Here’s what this looks like:

How CSS increments items

Notice the values of counter(item) that are shown in the <b> elements.

White Space Matters

Try adding a new line to the ruleset for the b::before selector:

b::before {
  counter-increment: item+1;
  content: "—[" counter(item) "]—";
}

You’ll see that the CSS elves also now happily add 1 for each <b> element they encounter. Notice that there is no space between item and +1.

Making <b> elements also increment item

But what if you change the + to a- sign?

b::before {
  counter-increment: item-1;
  content: "—[" counter(item) "]—";
}

The CSS elves get confused. They think that you are referring to a counter named item-1. If you’d used item-one, it would have had the same effect. You didn’t declare a counter called item-1, so they ignore it. The result will be just the same as what you saw in Figure 4.

If you add a white space, then everything is clear again.

b::before {
  counter-increment: item -1;
  content: "—[" counter(item) "]—";
}
No counter-decrement

Note that there is no counter-decrement property. You have to increment by a negative number. And a space.

Each b element decreases item by 1, in each and every case, even outside the <ul> where the item counter is declared. In effect, the CSS elves are very accommodating. You didn’t declare a counter for this particular element before you started using it? No worries, they will create one for you implicitly. Browsers are designed to be forgiving, so you can write sloppy code.

And yes, my code here is deliberately sloppy. I find that you learn more about how to write good code if you deliberately try to break the rules, and see what the effect is. If you always do things perfectly, you don’t find out what the rules are.

And, as before, each <li> item increases item by 5.

Alternately decrementing and incrementing a counter

Interacting with the User

A web experience cannot be considered to be a game if it has no user interaction. In this section, you’ll be seeing how to use checkboxes, radio buttons and the sibling combinators ( ~ and + ) to change what the CSS is counting, depending on what the user clicks on.

Not on display

The CSS elves will only count what they can see. You can try to trick them with another sloppy line of HTML:

<ul>
  <b></b>
  <li>For the money</li>
  <b></b>
  <input type="checkbox"> <!-- Linters don't like this in a <ul> -->
  <li>For the show</li>
  <b></b>
  <li>To get ready and go cat go</li>
  <b></b>
</ul>

And you can add a new rule that will hide the <li> item immediately after the checkbox when you check it.

(Note that I’ve also removed the counter-increment: item -1; from the b::before ruleset, to keep things simple.)

ul {
  list-style-type: none;
  counter-set: item 10;

  li {
    counter-increment: item 5;
  }

  li::before {
    content: "Item " counter(item) ". ";
  }

  input:checked + li {
    display: none;
  }
}

b::before {
  content: "—[" counter(item) "]—";
}

What do you think will happen when you click on the checkbox?

CSS doesn’t count elements that are not displayed

If an element is not displayed, then no counter will count it.

However, if you simply hide a list item, like this…

  input:checked + li {
    visibility: hidden;
  }

… then the CSS elves do count it:

CSS does count hidden items

So here’s a trick that you can use: You can set the display value of certain HTML elements to none if particular checkboxes or radio buttons are checked. The CSS elves will not count the elements with display: none, so you can display the number of such elements that are visible.

Time to play: counting clicks

If all this makes sense to you, the perhaps you’d like to create a really simple activity. (If it doesn’t make sense, then you can go back and read the links that I provided for each new concept, and you try to make different mistakes to irritate your browser, until you can work out what rules it really wants you to follow.)

This time, you can leave lists behind, and count checkboxes directly.

You can create an new HTML file and give it these elements:

  <label>
    <input type="checkbox">
    <span></span>
  </label>
  <label>
    <input type="checkbox">
    <span></span>
  </label>
  <label>
    <input type="checkbox">
    <span></span>
  </label>
  
  <p>You have clicked on <span>/</span> circles.</p>

Attach a CSS stylesheet with the following rules:

body {
  background-color: #222;
  color: #ddd;
  counter-set: total;
}

label span {
  --size: 20vh;
  display: block;
  width: var(--size);
  height: var(--size);
  border-radius: var(--size);
  background-color: gold;
  opacity: 0.25;

  counter-increment: total;
}  

input:checked ~ span {
  opacity: 1;
}

p span::before {
  content: "0"
}

p span::after {
  content: counter(total)
}

Make a prediction

Before you launch your new web page, do your best to predict what you will see:

You should see three pale gold circles, each with a checkbox at its top left, and text at the bottom of your page should say: “You have clicked on 0/3 circles.” You can click on the checkboxes and circles as much as you like. After the first click on any of the circles, it will become brighter, but the final sentence will not change.

Two circles clicked… but the text doesn’t update

The value 3 appears because of this rule:

p span::after {
  content: counter(total)
}
Add another counter

Can you imagine how to add another counter that will count how many <input> elements have been checked? Can you imagine how to display the value of this new counter in the ::before pseudo-element of the <p> element?

Can you do it on your own, without reading the solution below?

Solution

Here are the lines of CSS that I changed or added in my version, in order to make this feature work:

body {
  background-color: #222;
  color: #ddd;
  counter-set: total clicked;
}
<!-- lines skipped -->
p span::before {
  content: counter(clicked)
}

p span::after {
  content: counter(total)
}

input:checked {
  counter-increment: clicked;
}
Detecting how many circles were clicked

Now when you click on a gold circle, or on its checkbox, the number shown in the ::before pseudo-element changes.

More than one counter declaration?

You might think that declaring each counter on a different line might be valid CSS…

body {
  counter-set: total;
  counter-set: clicked;
}

… and it is valid, but it doesn’t behave the way you might expect. The second counter-set property overwrites the first, so counter-set: total; will be ignored.

This is the way CSS works for all other properties. In this ruleset…

label span {
  background-color: gold;
  background-color: silver;
}

… the silver background will overwrite the gold, just as you would expect.

If you want to declare different counter properties on different lines, just use a single semicolon at the end of the multi-line rule:

body {
  counter-set:
    total
    clicked;
}

One-way Clicking

If you click on the same gold circle a second time, the checkbox will be deselected, and the number of “clicked” circles will go down. What is the simplest way to ignore any subsequent clicks?

In a group of radio buttons, only one radio button can be checked at any given time. Clicking on that button again does not uncheck it. If you have a group with only one radio button in it, then you can switch it from unchecked to checked, but you can’t toggle it back. Initially, a group of radio buttons does not need to have any buttons checked. This means that when you use radio buttons, the initial state can have all the buttons unchecked, just like you can do with checkboxes.

To test this, change the type of each input from checkbox to radio and refresh your page.

  <label>
    <input type="radio">
    <span></span>
  </label>
  <label>
    <input type="radio">
    <span></span>
  </label>
  <label>
    <input type="radio">
    <span></span>
  </label>
  
  <p>You have clicked on <span>/</span> circles.</p>

Now only the first click on a gold circle will have any effect, and the number of “clicked” circles cannot decrease.

Not following orders

If you follow my instructions perfectly, your web page should work. But if it works, what exactly have you learnt? You have definitely learnt to follow instructions, but you probably knew how to do that already.

So what if you deliberately do something different, something disruptive, something that I didn’t ask you to do? For example: what would happen if you put the “You have clicked on … circles” message at the beginning of your HTML page?

And just as a double-check, you can add a new line between the second and the third label, like this:

  <p>You have clicked on <span>/</span> circles.</p>
  <label>
    <input type="radio">
    <span></span>
  </label>
  <label>
    <input type="radio">
    <span></span>
  </label>
  <p><span>/</span> so far</p>
  <label>
    <input type="radio">
    <span></span>
  </label>

Suddenly, your first message insists “You have clicked on 0/0 circles”, no matter how hard you click. The new line that you have inserted fails to count the circle in the layer above it, or any clicks on that circle.

The order of the HTML elements matters

But were you really being disruptive? Or were you just following instructions again? I wasn’t watching.

Above or below?

Notice how I use the word “above” in the question block above.

The new line that you have inserted fails to count the circle in the layer above it

The third circle is shown on the screen below the other two, and lower down in the HTML file. However, it is drawn in a layer that covers the elements that appear earlier in the HTML file. In the z-direction —from the screen to your eye— it is above the <p> element and the other circles, even if in the y-direction, it is closer to the bottom of your screen.

You can see what I mean if you add this rule at the end of your file:

label:last-of-type span {
  position: relative;
  top: -30vh;
  left: 10vh;
  border: 10px solid red
}
Elements lower in the HTML page are rendered above those higher in the page

So yes, in one sense, the bottom circle is “above” the others.

Order in HTML and CSS

From the test that you did in the last section, you can deduce that the order in which elements appear in the HTML file affects the order in which they can increment a counter.

But what about the order of rules in the CSS stylesheet? What happens if you change the order in which the counter properties are set and read, as shown below?

/* Read the value of the counter properties... */
p span::before {
  content: counter(clicked)
}

p span::after {
  content: counter(total)
}

/* ...before you declare the counter properties */
body {
  background-color: #222;
  color: #ddd;
  counter-set:
    total
    clicked;
}

/* The CSS below is unchanged */
label span {
  --size: 20vh;
  display: block;
  width: var(--size);
  height: var(--size);
  border-radius: var(--size);
  background-color: gold;
  opacity: 0.25;

  counter-increment: total;
}  

input:checked ~ span {
  opacity: 1;
}

input:checked {
  counter-increment: clicked;
}
Reordering the CSS has no effect

Does this solve the problem that you just created?

No. But if you place a copy of the element…

<p>You have clicked on <span>/</span> circles.</p>`

… back at the end (as I did for the screenshot above), then everything works fine, at least in this final position. This suggests that the order in which rules appear in the CSS file does not affect the order in which a counter is incremented.

But don’t let this give you a false sense of security. As Mr Weasley said: “Never trust something that can think for itself if you can’t see where it keeps its brain.”

Checking it twice

Whenever I am following a tutorial to learn a new technique, I do the tutorial twice. The first time, I follow the instructions carefully, so that I can see that the results are what the writer promised.

Sometimes, tutorials contain errors — a missing step, a missing line, a missing semicolon — and sometimes the technology has changed since the tutorial was written, and instructions that used to work no longer do. When I encounter a tutorial like this, I find that it is often worth persisting, and searching online for solutions. The process of searching is where a lot of my learning happens.

The second time I do the tutorial, I try not to read it at all. I do as much as I can from memory, and inevitably I make mistakes because there is something that I did not understand, or did not pay attention to, or assumed that I could ignore. I don’t mean to be disruptive. I just allow disruption to happen.

It is the mistakes that I make the second time that show me how much I still have to learn. If I don’t make these mistakes, I can deceive myself into thinking that I have understood. Solving my mistakes takes effort (especially if I don’t look back at the tutorial). And something that you have made an effort to do is something that you will remember.

An app that works perfectly the first time is less helpful than an app where I have to struggle to get it to work.

Traffic Lights, anyone?

A challenge for you

This arrangement of three circles, one above the other, might make you think of traffic lights. And you might be tempted now to get distracted and see if you can create a set of interactive traffic light with only CSS.

And why not? It’s fun to play, and play is great for learning.

The sequence of traffic lights is not the same in every country. Here’s a CSS-only illustration of the traffic light sequences in France and in the UK.

If you want to try this yourself, here’s a hint: You’ll be placing a checkbox <input> and a <span> inside a <label> for each light, as you have just been doing, but the <span> inside (at least) one label will need a z-index setting, so that a click on this <span> will check a radio button in a lower layer.

The GitHub octocat logo in the bottom right corner will take you to the repository where you can see how this CSS-only activity works.

But this article is about counting and you won’t need to count anything with traffic lights. So when you are ready, perhaps you would like to continue?

Custom CSS Properties

You can also use custom CSS properties to hold numbers. To create a custom CSS property, you use a property name that starts with --x, where “x” is any letter.

I’ve already used a custom CSS property to set the diameter of the circles in the last example:

label span {
  --size: 20vh;
  display: block;
  width: var(--size);
  height: var(--size);
  border-radius: var(--size);
  /* all other lines skipped */
}

A custom property lets you define a value once, and use it in many places. It helps you follow the DRY principle: Don’t Repeat Yourself. If you decided that you wanted circles of a different size, you could simply change the value of --size, in just one place, and the width, height and border-radius properties would all adjust their values.

My Opinion on the Term “CSS Variables

Some people use the term CSS variables as a synonym for custom properties. But I don’t.

In a programming language, a variable can have various values at different times. And so can custom properties. But:

  • A custom CSS property is often reset to its initial value, as you will see in a moment
  • You can’t display the value of a custom CSS property in the content property of a ::before or ::after element
  • A custom CSS property cannot be used recursively, to reset its value to a value calculated from itself, as the next demonstration shows.

In JavaScript, you can do this, and the JavaScript elves know how to handle it:

var x = 1
x = x * 2

After these two lines of code have been executed, x will have the value 2.

In CSS, you can set a custom property to a different value. This works fine:

label span {
  --size: 25vh;
  --size: 15vh;
  display: block;
  width: var(--size);
  height: var(--size);
  border-radius: var(--size);
  /* more lines skipped */
}

But in the snippet below, the second rule causes the CSS elves to panic, as if they see themselves in an infinity mirror. They simply stop working for you.

label span {
  --size: 25vh;
  --size: calc(0.6 * var(--size));
  display: block;
  width: var(--size);
  height: var(--size);
  border-radius: var(--size);
  /* more lines skipped */
}

Try both of these changes in the clicking-on-circles exercise that you have just done.

Custom CSS properties do not behave like variables do in other languages. So I always say “custom CSS properties”, to make this distinction clear. But if you call them “CSS variables”, if you want to. I won’t say anything. I’ll just sigh.

Here is the official explanation of this, without any reference to CSS elves: Resolving Dependency Cycles

Setting a starting value for a counter with a custom property

To explore how custom CSS properties can interact with counters, create a new HTML file with the following body:

  <b></b>
  <p>Tinker</p>
  <p>Tailor</p>
  <p>Soldier</p>
  <p>Sailor</p>
  <hr>
  <label>
    Add 10: <input id="add10" type="checkbox">
  </label>
  <label>
    Add 100: <input id="add100" type="checkbox">
  </label>
  <label>
    Add 1000: <input id="add1000" type="checkbox">
  </label>
Write your own CSS

Here’s a challenge: Before you read on, can you use everything you have learnt so far about CSS counters to write the CSS that will make your page look like this?

What your page should look like
Solution

Are you ready now? Or are you being disruptive, like I suggested you should be?

Did you write something like this?

body {
  counter-reset: item 
}

b::before {
  content: "—[" counter(item) "]—";
}

p {
  counter-increment: item;
}

p::before {
  content: counter(item) ": ";
}

label {
  display: block;
}

I imagine that:

  • You chose a different name for your counter
  • You might have given it an initial value and an explicit increment value
  • Your rules might be in a different order.

That’s good. It means that you won’t be able to simply copy and paste the CSS that I provide below. You’ll have to adapt it to work with your property names. You’ll have to make an effort. 😈

Activating the inputs

To activate the checkboxes, can you do these three steps?

  1. Add a custom CSS property to the ruleset for the body selector, and set its value to 0. Below, I’ve used the name --start, but you can use whatever name you like.
  2. Use your custom CSS property as the initial value for your counter.
/* Create a CSS property and use it to initialize the counter */
body {
  --start: 0;
  counter-reset: item var(--start);
}
  1. Add three new rules that will change the value of your custom CSS property. Note that I have placed the rule for the #add1000 input first. Please do the same, even if you are feeling disruptive. You’ll see why in a moment.
/* Use the checkboxes to change the value of --start */
body:has(#add1000:checked)  {
  --start: 1000
}

body:has(#add10:checked)  {
  --start: 10
}

body:has(#add100:checked)  {
  --start: 100
}

Notice that I have wrapped the reference to the <input> elements with the new :has(...) pseudo-class. Basically, this says: “if the body has a checked input, then set the value of --start for the body and all its children”.

Why use :has()?

What would happen if you don’t use the :has() pseudo-class, like this?

#add100:checked  {
  --start: 100
}

The rule above would say: “Set the value of --start for input#add100 and all its children.” But <input> elements don’t have any children, so the value of --start would change only for the <input> itself, and the <input> itself ignores it. So nothing would happen.

Testing what the checkboxes do

Refresh your page, and then click on each of the checkboxes in turn. You should see the numbers change. Do they change the way you would expect them to?

When you check Add 10, you should see ——[10]——. If you check both Add 10 and Add 100, you should see ——[100]——, not 110 And if you check Add 1000 on its own, you should see ——[1000]——.

But if you check Add 1000 and one of the other checkboxes, you do not see ——[1000]——. You see ——[100]—— or ——[10]—— instead.

Setting –start with a checkbox

If you see something different, then check that your rule for #add1000 does indeed appear first in your CSS file, as it does in my code listing above.

Order does matter in CSS

I deliberately placed the rule for #add1000 first, so that you can see that the order in which the CSS rules are written does matter when you set the value of custom CSS property. Even if you change the order of the <input> elements in the HTML file, as shown below, the order of the CSS rules still takes priority.

  <label>
    Add 1000: <input id="add1000" type="checkbox">
  </label>
  <label>
    Add 100: <input id="add100" type="checkbox">
  </label>
  <label>
    Add 10: <input id="add10" type="checkbox">
  </label>

Try it and see.

Resetting A Counter

As you have just seen, you can set a counter to a value given by a custom CSS property. If you change the value of the custom CSS property, the values of all the counters will be recalculated.

But you can also use counter-set explicitly anywhere in your CSS. Will there be a conflict of interests? Who will win? The custom CSS property or the value given by counter-set?

Here’s how you could test this.

  1. Add a new line to your HTML:
  <p>Tinker</p>
  <p>Tailor</p>
  <b class="resetter"> <!-- new line -->
  <p>Soldier</p>
  <p>Sailor</p>
  1. Replace your current rule for body with these two rules… which may or may not be in a logical order. (That’s what we want to find out.)
b.resetter {
  counter-reset: item 9999;
}

body {
  counter-reset: item var(--start);
  --start: 0;
}
/* The following lines do not change */
  1. Refresh your page. You should see something like this.
  1. Notice that the item number for Soldier is 10000 (9999 + 1), and that this value does not change when you check any of the checkboxes, although the numbers for the paragraphs before the b.resetter element do change.
counter-reset takes priority over custom properties

In the screenshot above, the custom CSS property --start is set to 100, and this determines what value the item counter has, until the <b class=resetter> element is displayed. At this point, the rule…

b.resetter {
  counter-set: item 9999;
}

… is applied, and the initial value set by the custom CSS property --start is forgotten. Once again, it is the order of elements in the HTML source that decides what value a counter will have at any particular point.

To summarize:

This is, in fact, logical, but it can be confusing.

Counting Without Numbers

So far, every time you’ve seen a counter, it’s looked like a number. Well… that’s not quite true. Right at the beginning, in the little sneak-peek demos, you saw counters that look like letters and emojis. You may not even have realized they were counter tokens, although I did give a big hint.

Here’s a CSS-only presentation of how the @counter-style at-rule can be used to change the tokens that are used to number items in an <ol> list. Click on the style names on the right to see how it changes the counter tokens on the left.

The <ol> list on the left contains only empty <li> items, so all you see are the ::marker tokens.

The default value for list-style-type is decimal. Your browser has over 100 built-in alternatives, to cater to writing systems all around the world. You can see them in action here. In order to create so many styles, the @counter-style at-rule became available in all major browsers since September 2023.

Here, for example, is the built-in @counter-style at-rule for Thai numeric symbols. This gives the Unicode code points for each numeral:

@counter-style thai { 
  system: numeric;
  symbols: '\E50' '\E51' '\E52' '\E53' '\E54' '\E55' '\E56' '\E57' '\E58' '\E59';
}

The @counter-style below has exactly the same effect. It uses the actual Thai characters instead of their code points.

@counter-style thai { 
  system: numeric;
  symbols: '๐' '๑' '๒' '๓' '๔' '๕' '๖' '๗' '๘' '๙';
}

And just like the counter property has been made generic so that all HTML elements have access to it, so the @counter-style has been made generic, so that you can create your own styles. Here’s my rule for English weekday abbreviations:

@counter-style weekdays {
  system: cyclic;
  symbols: "Mon" "Tue" "Wed" "Thu" "Fri" "Sat" "Sun";
  suffix: "";
}

By default, each counter-style has "." as its suffix. If you want to remove this (as I did for the weekdays), or change it to something else (like ")"), you can use the suffix property.

Because you can use any text, you can also use emojis. Or mathematical symbols. Or Unicode shapes and arrows. The W3C specifications also describe the use of images, but at the time of writing, the major browsers have not implemented this for counters yet. (You can set the list-style-image as bullet points for a whole list or for an individual list item, though, but this is outside the current topic of counters.)

::marker Styling

In the example in the previous section, all the Thai list-items appear in red, and the weekday abbreviations “Sat” and “Sun” appear in gold. This is not an effect of the @counter-style that is used. This is achieved by styling the ::marker pseudo-element.

You can only change a few properties of the ::marker pseudo-element. You can’t change its alignment or position, for instance, but you can change the color and even the content. Here’s the CSS that I’ve used to make weekends stand out, and react to being rolled over by the mouse:

#weekdays:checked {
  & ~ ol {
    list-style-type: weekdays;

    li:nth-child(7n + 6)::marker,
    li:nth-child(7n)::marker {
      color: gold;
    }

    li:nth-child(7n + 6):hover::marker,
    li:nth-child(7n):hover::marker {
      content: "Weekend!";
    }
  }
}

Note that the :hover pseudo-class applies to the list item itself; it cannot be applied to the ::marker pseudo-element.

Hovering over Saturday

The ::marker pseudo-element only applies to list elements (<ul>, <ul> and <dl>). You can provide CSS rulesets for the ::before and ::after pseudo-elements of any visible element, and these can be styled in many more ways than the ::marker pseudo-element.

No-click Actions

So far, you’ve seen how to count checkboxes, radio buttons and other elements which are displayed on the page. This requires the user to interact physically with your game, and interaction is what gives your players the feeling that they are in control. (But you know that you, the game developer, you are really the one who is in control of their experience.)

Often, though, you want certain actions of your game to happen automatically: a countdown timer, for example, or a spinning reel on a fruit machine, or the expression on an emoji face. Or you may want to trigger something without the player performing an action as deliberate as a conscious click.

In the rest of this article, I’ll describe two other solutions: CSS animations and the :hover pseudo-class.

Animations

The easiest project for exploring how CSS animations work with CSS counters is a countdown timer. Here’s the kind of thing you’ll be creating in this exercise (with some extra contral buttons):

For this new topic, you can create a new folder with the name “Countdown” and create inside it an HTML file and a linked CSS file. Here’s enough HTML to get started:

<div></div>

Here is some equally minimal CSS:

body {
  counter-set: countdown 10;
}

div::after {
  content: counter(countdown);
  font-size: 256px;
}
Connecting HTML and CSS

I’m assuming that you know how to create an HTML page with a linked CSS file, so that you can get this to work for you. If not, click on the GitHub octocat logo in the demo above, and check out how it works.

The font-size is not strictly necessary, but it makes the display more dramatic. Your page should now show a big number 10.

The @keyframes at-rule

CSS Animations are defined by a @keyframes at-rule. Here is a rule that creates an animation with the name timer:

@keyframes timer {
   0% { counter-increment: countdown   0; }
  10% { counter-increment: countdown  -1; }
  20% { counter-increment: countdown  -2; }
  30% { counter-increment: countdown  -3; }
  40% { counter-increment: countdown  -4; }
  50% { counter-increment: countdown  -5; }
  60% { counter-increment: countdown  -6; }
  70% { counter-increment: countdown  -7; }
  80% { counter-increment: countdown  -8; }
  90% { counter-increment: countdown  -9; }
 100% { counter-increment: countdown -10; }
}

You can add this at the end of your CSS file.

Hacking with Emmet

Note that I did not type all of this out by hand. I used a hack. My code editor is VS Code, and this comes with a built-in plug-in called Emmet. (Emmet is an old or dialectical word for ant.) Emmet provides shortcuts for HTML and CSS code that you might need to type regularly. As of the time of writing, it does not provide shortcuts for generating CSS keyframe animations. So I cheated. I used a shortcut for creating a series of HTML elements with incrementing values. I typed this in my HTML page…

div{ $% counter-increment: countdown -$ }*10

… and then pressed Enter. (Tab also works.) This produced the following output:

<div> 1% counter-increment: countdown -1 </div>
<div> 2% counter-increment: countdown -2 </div>
<div> 3% counter-increment: countdown -3 </div>
<div> 4% counter-increment: countdown -4 </div>
<div> 5% counter-increment: countdown -5 </div>
<div> 6% counter-increment: countdown -6 </div>
<div> 7% counter-increment: countdown -7 </div>
<div> 8% counter-increment: countdown -8 </div>
<div> 9% counter-increment: countdown -9 </div>
<div> 10% counter-increment: countdown -10 </div>

The curly {} brackets define the text that I want to appear inside the HTML element, the dollar $ sign is replaced by a line number and the multiplication * sign tells Emmet how many elements I want.

The output is not exactly what I want, but I then used VS Code’s built-in multi-cursor selection to quickly remove the parts that I did not want, to add curly {} brackets where I did want them, and to add a 0 between the numbers on the left and the following percentage % sign.

Absolutely not a loop

Notice that each keyframe deducts a bigger number from countdown than the previous keyframe. This reveals a critical difference between the way CSS applies a variety of rules to a static page, compared with the way JavaScript can apply loops and variables to create an endless range of possibilities.

In JavaScript, you could reduce the value of a variable by 1 on each iteration through a loop. In CSS, each time a new keyframe uses counter-increment to change the value of the countdown counter, it does this with reference to the initial value of 10 that is set by this rule for the <body> element:

body {
  counter-set: countdown 10;
}

Or to put it another way, CSS starts from the original static HTML page each time, and applies its rules in the appropriate order. There is never any question of looping.

The independence of @keyframes

An @keyframes rule simply gives instructions on what should change at what point in the animation. It does not give any information on which element this animation should be applied to, or how fast it should run or whether it should repeat, or a number of other things.

Playing an animation

To get the countdown timer to work, you can add a single line to your CSS. This provides details that are not included in the @keyframes at-rule itself:

div::after {
  content: counter(countdown);
  font-size: 256px;
  animation: timer 10s forwards;
}

This new line says:

The animation should start as soon as you load the page, and continue until the counter shows a big 0. The word forwards is one of the values of the animation-fill-mode property. It tells the CSS elves to leave everything the way they were told to by the last keyframe they read.

Transition between frames

Reload the page to restart your animation.

Hard refresh in Firefox

In Firefox, when you simply refresh a page, Firefox remembers the state of all your checkboxes, radio buttons, details elements and so on, and restores their state after refreshing the page. To clear all these states, you need to use a hard refresh, which also clears the cache.

The keyboard shortcut for this, for all common browsers, is Ctrl-Shift-R (or Command-Shift-R if you’re working on a Mac.)

Do you notice that the step from 10 to 9 plays faster than the following steps? This is because, by default, CSS applies a transition between each keyframe. If the property that the animation changes can have intermediate values, then the animation will move it smoothly from one keyframe to the next.

However, the value of your counter changes by a whole integer at each step. There are no intermediate values. With the default settings, the CSS elves get half-way towards the next keyframe and they think: “We’re closer to next waypoint now. Let’s change already!” As a result, with your counter, the changes happen after 0.5s, 1.5s, 2.5s, and so on. That’s why the change from 10 to 9 happens faster than you would expect.

You will see how to fix this in the coming sections.

What’s key about a keyframe?

The word keyframe is used to refer the precise values to use at precise times. The browser will interpolate other (non-key) frames between each keyframe.

Transitions

You can observe this interpolation process by adding a second @keyframes timer at-rule after the first:

@keyframes timer {
  0% { background-color: #f00; }
 10% { background-color: #f80; }
 20% { background-color: #ff0; }
 30% { background-color: #0f0; }
 40% { background-color: #0cf; }
 50% { background-color: #06f; }
 60% { background-color: #33f; }
 70% { background-color: #00f; }
 80% { background-color: #80f; }
 90% { background-color: #f0f; }
100% { background-color: #f00; }
}
Only the last timer animation is applied

Because this second at-rule appears later in the CSS file than the other, the CSS elves will ignore the first @keyframes timer. They won’t increment the countdown counter any more, they will only change the background-color.

Now when you refresh your page, you should see the background-color moving smoothly from one colour to the next. The effect should be similar to what you see in the animation below. (My animation has more bells and whistles than yours will have, to illustrate the various points I’ll be mentioning).

animation-timing-function

Actually, the default animation-timing-function is ease-in-out, so there it slows down as it reaches each intermediate background-color then speeds up again If you replace your animation rule with this…

  animation: timer 10s forwards linear;

… then the changes become even smoother.

Stepwise Animations

As I mentioned earlier, with the default animation-timing-function of ease-in-out (and even with the value linear), the changes in your countdown timer happen after 0.5s, 1.5s, 2.5s, and so on. For a countdown timer, this is not what you want. Here’s how to fix that.

Keep the @keyframes timer rule that changes the background-color, but change your animation rule to this:

  animation: timer 10s forwards step-end;

With the step-end timing function, you should see an abrupt change to the next background-colorat the end of every second. Comment out the background-color timer, so that the original counter-increment plays instead. You should see that change from 10 to 9 now happens the way you would expect.

Using otheranimation-timing-function values

What happens if you use jump-end, or end, or step-start as the timing function instead? Some of these have the same effect, one does something different.

Whenever you meet a new idea, it is good to try a number of variants. This helps you to remember. The human brain is very sensitive to changes. Your eyes are constantly twitching, so that the signal they send to the brain is always slightly different. If the image on your retina does not change, your vision becomes poorer. When the pump on your refrigerator is running, your brain stops hearing it. When the pump motor stops, you suddenly become aware of the lack of noise.

If you give your brain the chance to compare and contrast ideas, your brain will pay more attention.

Here’s another demonstration of the timing of changes of a different discrete animatable CSS property: z-index:

This demonstration uses the following @keyframes animation:

@keyframes z-left {
  0%  { z-index: 0; left:  0px; }
  10% { z-index: 1; left:  0px; }
  30% { z-index: 2; left:  50px; }
  50% { z-index: 3; left: 100px; }
  70% { z-index: 4; left: 150px; }
  90% { z-index: 5; left: 200px; }
 100% { z-index: 5; left: 200px; }
}

When you select step-end as the animation-timing-function for this animation, the integer steps for z-index coincide with the stepwise change of the left property for the grey square. When you select linear as the animation-timing-function, the value of left changes at a steady pace between keyframes. The change in z-index occurs when the grey square is halfway between two keyframe positions.

You can find a list of all animatable CSS properties here. Most properties have an animation associated with them, and code that you can copy and paste.

Starting an Animation

For now, to restart your animation, you have to reload the page. Reloading the page will reset everything in your game, so you only want to do this if the player wants to start over from the beginning.

Here’s a change you can make so that the animation will restart when you move your mouse over the <div> element. I’ve given the complete CSS code, so you can compare with what you have in your project, but only a small part has changed.

Note that I have reduced the duration of the animation to just 1s, because your time is precious, and you don’t want to wait ten whole seconds to see the final result.

body {
  counter-set: countdown 10;
}

div::after {
  content: counter(countdown);
  font-size: 256px;
}

/* animation rule moved to here... and made to run faster */
div:hover::after {
  animation: timer 1s forwards step-end;
}

@keyframes timer {
  0% { counter-increment: countdown   0; }
 10% { counter-increment: countdown  -1; }
 20% { counter-increment: countdown  -2; }
 30% { counter-increment: countdown  -3; }
 40% { counter-increment: countdown  -4; }
 50% { counter-increment: countdown  -5; }
 60% { counter-increment: countdown  -6; }
 70% { counter-increment: countdown  -7; }
 80% { counter-increment: countdown  -8; }
 90% { counter-increment: countdown  -9; }
100% { counter-increment: countdown -10; }
}

/* @keyframes timer {
   0% {  background-color: #f00; }
  10% {  background-color: #f80; }
  20% {  background-color: #8f0; }
  30% {  background-color: #0f0; }
  40% {  background-color: #0f8; }
  50% {  background-color: #08f; }
  60% {  background-color: #00f; }
  70% {  background-color: #80f; }
  80% {  background-color: #f0f; }
  90% {  background-color: #f08; }
 100% {  background-color: #f00; }
} */

Now, if you move your mouse over the text the <div>, the animation will start. It will stop again and revert to showing 10 as soon as you move the mouse down off the <div>.

Restarting when the state changes

If you hold the mouse in a place where the animation will run to the end, you should see a big 0. If you then move the mouse away from the link, it will revert to showing 10.

Before, it played all the way to the end, and then stayed at 0, regardless of where the mouse was. What has changed?

Before, the animation started as soon as the page was loaded. The page remained loaded after the animation finished, so the CSS elves left everything the way they were told to by the final 100% keyframe of the animation. Now, when you move the mouse off the link, they consider the animation to be switched off again, and reset everything to its original state.

Pausing an Animation

Instead of :hover, you can use the state of a checkbox or a radio button to change the state of an animation. Edit your HTML page so that it looks like this:

<label>
  <input type="radio" name="play-state" id="play">
  <span>Play</span>
</label>
<label>
  <input type="radio" name="play-state" id="pause">
  <span>Pause</span>
</label>
<label>
  <input type="radio" name="play-state" id="stop">
  <span>Reset</span>
</label>

<div></div>

Notice that there are three radio buttons which all appear before the <div>. These radio buttons share the same name property, so only one can be on at a time, but they all have different id values.

In your CSS page, comment out the rule for div:hover::after and add a new rule for the radio buttons:

/* div:hover::after {
  animation: timer 1s forwards step-end;
} */

input:checked {
  animation: timer 1s forwards step-end;
}

There are three radio buttons. If you check any of them, the countdown animation will play. The <div> still has a rule that tells its ::after pseudo-element to display the value of the counter(countdown), but it no longer owns the animation. The animation belongs to whichever radio button was last clicked. The countdown counter belongs to the <body> element, and the div::after element can listen for its current value, because it is a child of the <body>, which specifically appears in the HTML after any of the :checked radio buttons.

Restoring ownership

To give ownership back to the div::after element, you can use the selector below:

body:has(input:checked) div::after {
  animation: timer 1s forwards step-end;
}

The selector body:has(input:checked) div::after says: “Apply the following rule to any div::after element when the <body> contains an <input> that is :checked.” All the radio buttons appear before the <div> element, so if any of them is checked, the animation rule will apply.

And now, with the body:has(...) function, it doesn’t actually matter whether the <div> appears before the radio buttons or after.

Reload your page so that no radio buttons are checked, and then click any one of them. The animation plays to the end, and stops on 0. Now click on any of the other radio buttons.

Nothing happens.

The rule says: “If any of the radio button <input>s is :checked, play the animation. If not, stop the animation.” So it doesn’t matter which button is checked. They all agree with each other. The <div> receives the same message from each radio button: “Go on! Play your animation!” But after the first message, the <div> simply shrugs: “I have played it already. I’m good.”

What happens if you specify that only the button with an id of play is to make the animation play? Try this:

body:has(input#play:checked) div::after {
  animation: timer 1s forwards step-end;
}

Now, if you click on the first button, the animation plays to the end. If you click on one of the other buttons, the display reverts to showing 10.

Activating the Pause button

Add a ruleset for the #pause button, as shown below. Note that I have reset the duration for both buttons to the same value of 10, so that you have time to click on different buttons while the animation is in progress.

body:has(input#play:checked) div::after {
  animation: timer 10s forwards step-end;
}

body:has(input#pause:checked) div::after {
  animation: timer 10s forwards step-end;
  animation-play-state: paused;
}

The new input#pause selector also plays the animation when its button is selected, but it sets the animation-play-state to paused. So if you refresh your page and then click the middle radio button, nothing happens.

The CSS elves are in fact busy, but they are busy doing nothing.

If you click on the Play radio button, the animation will start. If you click on the Pause button, it will … pause. You can continue the animation from this by clicking on the Play button again. Or you can reset the counter to 10 by clicking on the Reset button.

Who Cares Who Owns the Animation

What happens if you comment out the line that makes the #paused play the animation?

body:has(input#pause:checked) div::after {
  /* animation: timer 10s forwards step-end; */
  animation-play-state: paused;
}

If you start the animation with the first button and then click on the Pause button, the animation reverts to its initial state, just as if you had clicked on the Reset button. The animation-play-state no longer knows whose animation it refers to.

Here is a more elegant version of the working CSS:

body:has(input#play:checked) div::after,
body:has(input#pause:checked) div::after {
  animation: timer 10s forwards step-end;
}

body:has(input#pause:checked) div::after {
  animation-play-state: paused;
}

Why is this more elegant? DRY! (Don’t Repeat Yourself)

The duration of 10s is defined in only one place. In the earlier version, you could set a different duration in each of the rulesets, and unexpected things could happen. (I’m guessing that they did, and you had to puzzle about what had gone wrong for a while. I did warn you, but I’ve also suggested that you should be disruptive. I can’t have it both ways, can I?)

The selector that you use for the different rules must apply to exactly the same element. You can say that the animation “belongs” to that element. The selectors used can look very different, so long as they refer to the same animated element. These rules would also work:

label:has(input#play:checked) ~ div::after,
body:has(input#pause:checked) div::after{
  animation-name: timer;
  animation-duration: 10s;
  animation-fill-mode: forwards;
  animation-timing-function: step-end;
}

label:has(input#pause:checked) ~ div::after {
  animation-play-state: paused;
}

A Reset Button

Now that you have buttons to control the animation, you can use a <button type="reset"> inside a <form> element to reset the buttons to their initial states. A <button type="reset"> on its own won’t work: it has to be inside a <form> element before it has any effect.

Here’s what your HTML might look like with a form reset button:

<form>
  <label>
    <input type="radio" name="play-state" id="play">
    <span>Play</span>
  </label>
  <label>
    <input type="radio" name="play-state" id="pause">
    <span>Pause</span>
  </label>
  <label>
    <input type="radio" name="play-state" id="stop">
    <span>Reset</span>
  </label>

  <div></div>

  <button type="reset">Reset Form</button>
</form>
Edge case

This only works because the animation is triggered by buttons. That’s why I did not suggest it from the beginning. An animation that is triggered automatically after the page has loaded will continue to play, no matter how hard you press a <button type="reset">.

Triggering a Timeout

Now that you can create a countdown timer, the next step is to do something when the player’s time runs out. If CSS were a programming language, you might do something when the value of the counter reaches 0. But CSS is a styling language. It simply applies a series of rules to a basically static page. It uses counters but it does not give you any way to read the value of a counter, and certainly no way to compare one value with another.

Off at a tangent

One main focus of this article is counters, but to bring this part of the story of a countdown to its logical conclusion, I’m going to have to leave counters aside for a moment, and talk about animations and other CSS properties, including custom properties that do not have a numerical value.

You’ve used the forwards value of the animation-fill-mode property, so whatever CSS rules are given by the last keyframe will persist after the animation has finished. To see this, add a new rule at the end of the @keyframes timer:

@keyframes timer {
  /* 10 lines skipped */
  100% { counter-increment: countdown -10;
         color: red;
       }
}

Click on the Reset radio button and then on the Play button. Wait 10 seconds, and watch how the number 0 is now shown in red when the animation ends.

Can you see where this is going? Instead of simply changing the color of the animated element itself on the last keyframe, you can change something more radical which will affect other elements. It would be best to start a new project for this.

Time to play: A Race Against Time

I want to use CSS to tell a story of how one person can save the whole world as we know it browser from a terrible simulated threat, by clicking on a button, in a race against time.

You can play this mini game below. The chances are that it’s already game over for you by the time you have read this far, but you can simply click on the Replay link:

Creating your own version of the game

Create a new HTML file with a linked CSS file. Here’s the HTML that you can use:

  <form>
    <label>
      <input type="checkbox">
      <span></span>
    </label>
    <div>It was always too late.</div>
    <button type="reset">Reset</button>
  </form>

You can make both the label and the div fill the entire browser viewport, with the div in front:

label,
div {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 7vmin;
}

div {
  background-color: #900;
}

The text “It was always too late” should now appear in dramatic black characters on an ominous red background. The mini-game that you can create will give the player the chance to prevent this terrible news from appearing.

The <label> with a transparent background is hidden behind this. However Reset button is also hidden behind the <div>, because of the use of position: fixed, which creates a new stacking context. To raise the button above the fixed elements, you have to give it a position other than relative or static:

button {
  position: absolute;
}

Hiding and showing the message of doom

To hide the red <div> you can set its display to none. But you don’t need to do this directly. You can use a custom CSS property with the value of none. Add a new rule, and a new line to the ruleset for the div:

body {
  --display: none;
}

label,
div {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 7vmin;
}

div {
  background-color: #900;
  display: var(--display);
}

button {
  position: absolute;
}

You should now see a little lonely checkbox in the centre of your page. If you click on it, it becomes selected but nothing else happens. You can add a rule to change the value of the --display custom property. This is not the rule that you are looking for:

input:checked {
  --display: flex;
}

You can try it anyway, but it won’t work. Why not?

Which element is this custom property applied to? Which element should it be applied to, so that the div rule can use it?

Try this instead:

body:has(input:checked) {
  --display: flex;
}

Do you see the difference? In the first rule, --display was set for the <input> element. The <div> is not a child of the <input> element, so it cannot inherit the value of --display.

In fact, <input> elements cannot have any children. They are written with a single self-closing tag, so there is nowhere for a child to be added. And for this reason, they cannot have ::before or ::after pseudo-elements either. That’s why I added an empty <span></span> element inside the label. A <span> can have children —and ::before and ::after pseudo-elements— and that’s good because that’s what I’ll use for the storytelling.

The second rule, which uses the :has() pseudo-class, allows me to select the <body> element, only if there is an <input> which is :checked as one of its children. When you check the checkbox, the <body> gives its --display custom property the new value of flex, and the <div> is a child of the <body>, so it inherits this new value. And the red message fills the screen.

You can click on the Reset button to make it disappear again.

Ten Seconds until the End

Actually, so far you have created the exact opposite of the story I want to tell. I want to create a countdown where clicking this button will stop the red message from appearing. This is the ending I want your triumphant player to see:

span::before {
  content: "My hero!";
  display: block;
}
span::after {
  content: "You saved us!"
}

You can use the same countdown timer that you used in the last exercise, with one little difference…

@keyframes timer {
   0% { counter-increment: countdown   0; }
  10% { counter-increment: countdown  -1; }
  20% { counter-increment: countdown  -2; }
  30% { counter-increment: countdown  -3; }
  40% { counter-increment: countdown  -4; }
  50% { counter-increment: countdown  -5; }
  60% { counter-increment: countdown  -6; }
  70% { counter-increment: countdown  -7; }
  80% { counter-increment: countdown  -8; }
  90% { counter-increment: countdown  -9; }
 100% { counter-increment: countdown -10;
        --display: flex;
      }
}

Instead of making the little harmless 0 turn red, this will turn the whole page red, as --display is set to --flex on the last keyframe.

If you add this to your CSS file, nothing will happen… yet. First you have to start the animation.

Where to add an animation rule

Here’s a question for you. The following rule will start the animation. Which element should it be applied to?

animation: timer 1s step-end forwards;

I’ve used a very short duration (1s) for now, so that you will see immediately if the animation is working or not. Remember, the final value of --display needs to be applied to the <div>. Would this work?

div {
  animation: timer 1s step-end forwards;
  background-color: #900;
  display: var(--display);
}

You can try it. You can try putting the new animation rule at the end of the ruleset. But, no, it doesn’t work.

It’s not crystal clear why not. The official documentation talks about “animation-tainted” custom properties, and discussions on the W3C GitHub Issues pages show just how complex the interaction of custom variables and keyframe animations is. I use the principle of Occam’s Razor here. To paraphrase: if it doesn’t work, don’t look for anything more complex. Find a simple solution that does work.

Solution

What happens if you apply the animation to the <body>instead?

body {
  --display: none;
  animation: timer 1s step-end forwards;
}

Now the animation works. Now that you know that, you can set its duration to 10s to give yourself time to stop the animation before it ends in tragedy.

Averting the Impending Disaster

My story needs two more elements to make it even a little bit interesting:

  1. An indication that the end is coming
  2. An action that will stop the animation.

You can use the counter(countdown) to indicate how much time is left:

input:not(:checked) {
  & ~ span::before {
    content: "Click me! Before it's too late...";
  }

  & ~ span::after {
    content: "We have only " counter(countdown) " seconds left!";
  }
}

The selector input:not(:checked) adds greater specificity to the selector that acclaims the hero (span::before), so the span will show this text as long as the checkbox is not clicked.

But counter(countdown) starts at 0 and then goes negative. How can you get it to start at 10? You’ll need to add the following rule:

body {
  --display: none;
  counter-reset: countdown 10;
  animation: timer 10s step-end forwards;
}

With this rule the countdown will be shown, and at 0, the “It was always too late” message will be shown.

However, if you click on the button to stop it, you just bring the end closer. That’s because of the rule you added a while back. It’s time to remove this:

/* body:has(input:checked) {
  --display: flex;
} */

Now, if you click (anywhere) the checkbox will be checked, and you will be praised as a hero.

Unavoidable disaster?

But… wait for the 10-second countdown to end, and the terrible red message will appear anyway.

You need to stop the animation.

Only you can do this. You are the hero.

You need to tell the <body> to pause the animation when the checkbox is checked.

And here comes the twist in my story. You are not the hero after all. You just set up the situation so that you can look like the hero.

Does that give you a clue as to how to solve this?

You need to set the animation-play-state to paused by default, and only set it to whatever the opposite of paused is while the checkbox is not checked. This means that you need to create a selector that selects the <body> only if the checkbox is not checked.

Your secret Bond-villain plan

Here’s one way to look like a hero:

body {
  --display: none;
  counter-reset: countdown 10;
  animation: timer 10s step-end forwards paused;
}

You make the world safe. And then you create the sense of danger with this new rule:

body:has(input:not(:checked)) {
  animation-play-state: running;
}

And since you are now the villain, you can deselect the checkbox again, and let countdown continue. You can both save the world and let the end come anyway.

Yes, but while you were doing this, the real hero (played by your non-evil twin) crept unseen into the HTML file and changed the type of the <input> from "checkbox" to "radio". Once a solitary radio button is checked, it cannot be unchecked. The good guys win again!

One person can save the world!

<form>
  <label>
    <input type="radio">
    <span></span>
  </label>
  <div>It was always too late.</div>
  <button type="reset">Reset</button>
</form>

But… Reset Spells Disaster!

Ha! Your non-evil twin has missed a trick! Press the Reset button, and the countdown timer continues!

This is because resetting the form resets the state of the input elements inside the <form>, and this unchecks the radio button, just as if it were a checkbox.

You need something more radical than simply resetting the form. The simplest solution is to reload the whole page, using a link whose href is /. Notice that you don’t need the <form> element any more. Here’s the final HTML:

<label>
  <input type="radio">
  <span></span>
</label>
<div>It was always too late.</div>
<<a href="/" draggable="false">Replay</a>

To prevent the link from moving when dragged, just like a button, I’ve added draggable="false".

Here’s the final CSS, where I’ve replaced the ruleset for button with a nested ruleset for a, to make the link look as much like a standard button as possible.

body {
  --display: none;
  counter-reset: countdown 10;
  animation: timer 10s step-end forwards paused;
}

body:has(input:not(:checked)) {
  animation-play-state: running;
}

label,
div {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 7vmin;
}

div {
  background-color: #900;
  display: var(--display);
}

a {
  position: absolute;
  padding: 0.1em 0.2em;
  background-color: #eee;
  border: 1px outset #999;
  border-radius: 0.25em;
  text-decoration: none;
  color: #000;

  -webkit-user-select: none;
  -khtml-user-select: none;
  -moz-user-select: none;
  -o-user-select: none;
  user-select: none;

  &:hover {
    background-color: #ddd;

    &:active {
      background-color: #ccc;
      border-style: inset;
    }
  }
}

span::before {
  content: "My hero!";
  display: block;
}
span::after {
  content: "You saved us!"
}

input:not(:checked) {
  & ~ span::before {
    content: "Click me! Before it's too late...";
  }

  & ~ span::after {
    content: "We have only " counter(countdown) " seconds left!";
  }
}

@keyframes timer {
   0% { counter-increment: countdown   0; }
  10% { counter-increment: countdown  -1; }
  20% { counter-increment: countdown  -2; }
  30% { counter-increment: countdown  -3; }
  40% { counter-increment: countdown  -4; }
  50% { counter-increment: countdown  -5; }
  60% { counter-increment: countdown  -6; }
  70% { counter-increment: countdown  -7; }
  80% { counter-increment: countdown  -8; }
  90% { counter-increment: countdown  -9; }
 100% { counter-increment: countdown -10;
        --display: flex;
      }
}

Final Tweaks

The online versionof this mini story game contains some cosmetic improvements. In particular, if you let the countdown reach one second, your version of the game will say “We have only 1 seconds left!”, with “seconds” in the plural even though there is only one.

1 second

Just 1 second

In the online version, this infelicity has gone. How do you think I fixed it?

Hint: counters can count.

Also, if the text wraps, then the line break will not occur between the number and the word “second”. For this I’ve used a non-breaking space, which for CSS needs to be written as "\00a0". You can click on GitHub’s Octocat logo to visit the repository where you can find the complete source. Or you can look at the solution below.

Solution
body {
  --display: none;
  counter-reset:
    countdown 10
    second 2 /* used with the seconds counter-style */
  ;
  animation: timer 10s step-end forwards paused;
}

/* Create a counter-style to use "2 seconds" and "1 second"
 * correctly. Use a non-breaking space between the number
 * and "seconds" so that the whole chunk "X seconds" will 
 * wrap to the next line together.
 */
@counter-style seconds {
  system: cyclic;
  symbols: "\00A0second" "\00A0seconds";
}
/* lines skipped */
span::before {
  content: "My hero!";
  display: block;
}

span::after {
  content: "You saved us all... with only " counter(countdown) counter(second, seconds) " to go!"
}

input:not(:checked) {
  & ~ span::before {
    content: "Click me! Before it's too late...";
  }

  & ~ span::after {
    content: "We have only " counter(countdown) counter(second, seconds) " left!";
  }
}

Animating a counter-style

Now you are ready to combine animating a counter and using a custom @counter-style at-rule. Here’s a little puzzle where the challenge is not finding the combination of letters that opens the lock, but finding the order in which you need to change the letters. If you get the right word but don’t get the right order, the CSS equivalent of an alarm goes off when you press Enter.

But you should know a little about me by now. I do like words to be spelt correctly, and I do like to move the shortest distance between points. Of the 24 possible orders in which you can change each letter only once, only one meets my high standards.

This article is about counters, so I won’t show you how to create the entire puzzle. I’ll just show you how to animate the way the letters change. I’ll get you to make a much simpler activity that uses this technique. If want to see how the order-of-execution logic was done, you can check out the GitHub repository.

Another new project

It’s time to start a new project with a basic HTML page linked to a CSS file, just like you’ve done before. Create a new folder called Animated Counters, with and index.html file linked to a styles.css file, And then put on your Thinking Cap.

A checkbox and a span (or two)…

You’re going to be animating two counters, using letters instead of numbers. The animation will start when you check a checkbox input. The image below shows what I want your page to look like in a browser. Do you think you can create this on your own, without looking at the HTML and CSS below?

Before the animation: Hi

As a clue, here’s how your page will will look like after the animation (but you don’t have to think about the animation yet):

After the animation: By

OK, I do not normally spell “Bye” this way, and if you do you might accuse me of cultural appropriation, but please forgive me. Animating just two letters is all you need to understand the techniques.

“H” is the eighth letter in the alphabet, and “i” is the ninth. Later (but not yet), you’ll create one @keyframes animation to count from 8 to 2 (to replace “H” with “B”), and another@keyframes animation to count from 9 to 25 (to replace “i” with “y”).

Do your best with creating the “Well Hi! page! You can compare your version with mine later, and see if yours was in fact more elegant : )

Hints

Here are some things to think about:

  • How to show the value of a counter
  • How to set the initial value of a counter
  • How to display a counter using upper-case letters
  • How to display a counter using lower-case letters
  • How you will (eventually) hide the checkbox input
  • How you can make a label look like a button
  • The final exclamation mark is not going to change. It’s not part of either animation.
Solution

Here’s the HTML that I used:

<label>
  <input type="checkbox">
  Well
</label>
<span></span>!

And you’ll see below the CSS that I started with. Note that the names for built-in counter styles is case-insensitive. I could use upper-alpha or Upper-Alpha and it would work just as well.

body {
  counter-set:
    letter-one 8
    letter-two 9
  ; 
}

label {
  padding: 8px 16px;
  border: 1px outset #888;
  border-radius: 8px;
  display: inline-block;
}

input {
  position: absolute;
  left: 0vw;
}

span::before {
  content: counter(letter-one, UPPER-ALPHA);
}

span::after {
  content: counter(letter-two, lower-alpha)
}

I imagine that you will have used different names for your classes and counters. You might have used a different selector to hold the counter-set rule. You might have used two elements to show the letters Hi, each with its own ::before or ::after pseudo-element.

That’s good. If your CSS creates a similar effect to mine, don’t change it at all. If your CSS is different now, that means that you will have to adapt the CSS that I provide below so that it works with yours. And that means that you might make mistakes which you will have to correct. And each mistake that requires effort to correct helps you to remember what you have learnt.

I’ve used left: 0vw for the absolute position of the input element. That means that it remains visible on the page for now. In production, I would set it to -999vw which will move it far off-screen to the left. The value I use is arbitrary; all that matters is that it is greater than the screen width of a checkbox.

If you want your label to behave more like a button when you hover your mouse over it or click, you can add a couple of cosmetic rules:

label {
  -webkit-user-select: none;
  -khtml-user-select: none;
  -moz-user-select: none;
  -o-user-select: none;
  user-select: none;
}

label:has(:checked),
label:hover:active {
  border-style: inset;
  background-color: lightgrey;
}

With these changes, the label appears in a different state when the <input> that it contains is :checked.

Treating the animation

Figure 14 below shows your web page should look like after you check the checkbox.

Click the checkbox to run the animation: By

So here’s a second challenge.

Create two @keyframes at-rules

Can you create two @keyframes at-rules, one to change the letter H into the letter B, and one to change the letter i into the letter y?

And can you find a CSS selector and ruleset that will do the following?

  • Detect when the <body> element contains a checked input
  • Tell each of the ::before or ::after pseudo-elements which @keyframes animation to use
Solution

Here are the @keyframes at-rules that I used in my version:

@keyframes letter-1 {
  0% { counter-increment: letter-one  +0 } /* H */
  5% { counter-increment: letter-one  +1 } /* I */
 10% { counter-increment: letter-one  +2 } /* J */
 15% { counter-increment: letter-one  +3 } /* K */
 20% { counter-increment: letter-one  +4 } /* L */
 25% { counter-increment: letter-one  +5 } /* M */
 30% { counter-increment: letter-one  +6 } /* N */
 35% { counter-increment: letter-one  +7 } /* O */
 40% { counter-increment: letter-one  +8 } /* P */
 45% { counter-increment: letter-one  +9 } /* Q */
 50% { counter-increment: letter-one +10 } /* R */
 55% { counter-increment: letter-one +11 } /* S */
 60% { counter-increment: letter-one +12 } /* T */
 65% { counter-increment: letter-one +13 } /* U */
 70% { counter-increment: letter-one +14 } /* V */
 75% { counter-increment: letter-one +15 } /* W */
 80% { counter-increment: letter-one +16 } /* X */
 85% { counter-increment: letter-one +17 } /* Y */
 90% { counter-increment: letter-one +18 } /* Z */
 95% { counter-increment: letter-one  -7 } /* A */
100% { counter-increment: letter-one  -6 } /* B */
}

@keyframes letter-2 {
  0% { counter-increment: letter-two  +0 } /* i */
  6% { counter-increment: letter-two  +1 } /* j */
 12% { counter-increment: letter-two  +2 } /* k */
 19% { counter-increment: letter-two  +3 } /* l */
 25% { counter-increment: letter-two  +4 } /* m */
 31% { counter-increment: letter-two  +5 } /* n */
 37% { counter-increment: letter-two  +6 } /* o */
 44% { counter-increment: letter-two  +7 } /* p */
 50% { counter-increment: letter-two  +8 } /* q */
 56% { counter-increment: letter-two  +9 } /* r */
 63% { counter-increment: letter-two +10 } /* s */
 69% { counter-increment: letter-two +11 } /* t */
 75% { counter-increment: letter-two +12 } /* u */
 82% { counter-increment: letter-two +13 } /* v */
 88% { counter-increment: letter-two +14 } /* w */
 94% { counter-increment: letter-two +15 } /* x */
100% { counter-increment: letter-two +16 } /* y */
}

I could have used the same names for the animations as for the counters. CSS will understand that @keyframes letter-one refers to something different from counter(letter-one), but if I give them different names then you can see that the name of the animation and the name of the counter it increments can be different.

I’ve used intervals that are not exactly the same between each frame, just because I like to keep things as simple as possible.

Also, I made both animations run forwards through the alphabet, even though running backwards would have required fewer changes. It’s my homage to the pre-digital flight information display boards in airports, I suppose.

Flight information display board

Back to the @keyframes at-rules, notice how the “H to B” animation flips from positive increments to negative increments when it goes from “Z” round to “A”.

Here’s the CSS rule that I used to trigger the animations:

body:has(:checked) {
  span::before {
    animation: letter-1 2.0s linear forwards;
  }
  span::after {
    animation: letter-2 1.6s linear forwards;
  }
}

I used 0.1s for each letter change, because I just want to see that the animation is working without wasting precious seconds.

To be honest, if I really wanted not to waste time, I should run both animations backwards. Here is another way of achieving the same overall effect, this time with one animation finishing before the other begins:

@keyframes letter-a {
  0% { counter-increment: letter-one -0 } /* H */
 16% { counter-increment: letter-one -1 } /* G */
 33% { counter-increment: letter-one -2 } /* F */
 50% { counter-increment: letter-one -3 } /* E */
 67% { counter-increment: letter-one -4 } /* D */
 84% { counter-increment: letter-one -5 } /* C */
100% { counter-increment: letter-one -6 } /* B */
}

@keyframes letter-b {
  0% { counter-increment: letter-two  -0 } /* i */
 10% { counter-increment: letter-two  -1 } /* h */
 20% { counter-increment: letter-two  -2 } /* g */
 30% { counter-increment: letter-two  -3 } /* f */
 40% { counter-increment: letter-two  -4 } /* e */
 50% { counter-increment: letter-two  -5 } /* d */
 60% { counter-increment: letter-two  -6 } /* c */
 70% { counter-increment: letter-two  -7 } /* b */
 80% { counter-increment: letter-two  -8 } /* a */
 90% { counter-increment: letter-two +17 } /* z */
100% { counter-increment: letter-two +16 } /* y */
}

body:has(:checked) {
  span::before {
    animation: letter-a 0.6s linear forwards;
  }
  span::after {
    animation: letter-b 1.0s 0.6s linear forwards;
  }
}
A temporary alternative

Note that if you add this alternative solution, you don’t need to comment out the previous body:has(:checked) ruleset. CSS will apply the last rules given for this selector.

But after you’ve tested this alternative, delete it or comment it out. The rest of this exercise assumes that you are using the letter-1 and letter-2 @keyframes.

In animation: letter-b 1.0s 0.6s, the second time (0.6s) refers to the animation-delay property, which tells the animation how long it should wait before it starts. (You can even use negative values for this, and the animation will play as if it had begun in the past.)

Notice that in the @keyframes letter-1 at-rule above, the increment value jumps from +18 for Z to -7 for A. The same is true for the @keyframes letter-1 rule, which flips from to -8 for a to +17 for z.

@counter-style systems

The built-inlower-alpha and upper-alpha counters uses system: alphabetic in their definition. This is slightly different from system: numeric in that there is no value for zero. If you ask use a content declaration to show the value 0 for an alphabetic counter, it fall back on the @default counter-style and show exactly 0.

Because there are 26 letters in the English/Latin alphabet, the value 27 will be represented as AA or aa. This is not what you want in this particular case, but you might find it useful in other cases.

Try it and see. Change the values that you use in your counter-set rule to something like this:

body {
  counter-set:
    letter-one 0
    letter-two 27
  ; 
}
Letter 0 is “0” and letter 27 is “aa”
Fruit machine

The fruit machine animation that you saw at the beginning uses exactly the same technique, with a few extra tricks:

  • I’ve used an @counter-style at-rule that shows chosen emojis
  • Each reel of the fruit machine shows several numbers
  • The reels themselves are animated, to rotate about the x-axis at a regular speed, then to jump backwards each time the counter animation changes its value.
  • The smiley face also uses a counter and an @counter-style at-rule

There are also two Spin buttons for each reel. The second button is hidden until the first Spin button for two other reels has been triggered. This means that the third Spin button you press triggers a previously hidden button, which starts a different animation… which glitches.

So you can never win.

Running an Animation Backwards

If you’ve got to this sections, I’ll assume that you have solved the challenges in the last section, or have worked through the solutions that I proposed.

So now, if you deselect the checkbox, the display jumps back immediately to showing Hi. There’s no reverse animation.

Playing animations smoothly in reverse

Nikola Đuza at Pragmatic Pineapple has some good suggestions on how to make animations play smoothly in reverse, but none of them are helpful here.

If you look at the documentation for animation-direction, you’ll see that one of the options for the value is reverse. You might think, like I did, that it would be easy to use this value with the @keyframe animations that you have already created. After trying, trying and trying this again and again, it seems that I can’t force browsers to change the animation-direction of an animation that has already been applied. I have had to use a more brute-force solution. (If you find a better one, let me know.

My solution was to make an exact copy of my @keyframes letter-1 and @keyframes letter-2 animations. I called them @keyframes letter-3 and @keyframes letter-4. (I won’t show them here, because they are exactly the same as the animations you can find in the solution to the last challenge, just with different names.)

I added the rule below to my CSS. Notice the word reverse used to set the animation-direction.

body:has(input:not(:checked)) {
  span::before {
    animation: letter-3 2.0s linear reverse forwards;
  }
  span::after {
    animation: letter-4 1.6s linear reverse forwards;
  }
}

You can restore the original values for counter-set (8 and 9), and try this for yourself.

An unwanted side effect

You’ll see that it has an unwanted side effect. Because the checkbox <input> starts off unchecked, this reverse animation plays immediately after the page is loaded.

What you need is a third state, which will not trigger any animations at the start.

body:has(input…)

Did you notice that my earlier selector was simply body:has(:checked), but this selector includes input?

body:has(input:not(:checked))

If you don’t include input, the animation triggered by the first selector you added will not run.

You might think that adding input increases the specificity of this second selector, but that’s not the reason. The selector body:has(:not(:checked)) means that if any element does not have a :checked pseudo-class (even elements that do not ever receive a :checked pseudo-class), then this selector will apply to the <body> element.

If you place this rule after the previous one, it will take precedence because it is read later from the CSS file. You could use this less specific selector and put it before the one you added first, but a co-worker might think that logically it should be placed after, and move it, and break your CSS.

By adding the input element into the selector, you tell CSS to ignore any other kind of elements that have no :checked pseudo-class. Now your co-worker can reorder your rules in the CSS file and they will still work.

An alternative would be to add input only to your first selector . Try this:

body:has(input:checked) {
  span::before {
    animation: letter-a 0.6s linear forwards;
  }
  span::after {
    animation: letter-b 1.0s 0.6s linear forwards;
  }
}

body:has(:not(:checked)) {
  span::before {
    animation: letter-3 2.0s linear reverse forwards;
  }
  span::after {
    animation: letter-4 1.6s linear reverse forwards;
  }
}

You see? This also works, and this time it is because of specificity. Perhaps the best solution is to use input in both selectors, so that your co-workers can’t complain of you taking confusing shortcuts.

Preventing the unwanted animation on page load

So back to the question: How can you stop the reverse animation from playing automatically when the page is loaded? Can you think of any three-state solutions? There’s a hint in the subtitle below.

See if you can find a solution for yourself before you read on.

Radio buttons are off by default

In your HTML file, replace the single checkbox with these two radio button:

  label>
    <input type="radio" name="play" id="well">
    Well
  </label>
  <label>
    <input type="radio" name="play" id="good">
    Good
  </label>
  <span></span>!

Both buttons share the same name property, so only one can be on at any given time. Two states. But… both buttons can be off when the page is first loaded. Two buttons with a third state, for free!

So what CSS would you use with this? Notice that the radio buttons have different id values. Comment out your existing rules for body:has(...) (so that they don’t also get triggered) and add this new one:

body:has(#well:checked) {
  span::before {
    animation: letter-1 2.0s linear forwards;
  }
  span::after {
    animation: letter-2 1.6s linear forwards;
  }
}

This is how your page should look immediately after it’s loaded. There shouldn’t be any animation.

No checked radio buttons, no animation

And this is how it should look after your check the Well button, and after the animation has finished.

The Well radio button checked, animation complete
Just for fun

On my screen, the Well label is grey, which shows that its <input> has been :checked, but the radio button beside it appears unchecked. Can you explain why?

Hint: How many radio buttons are there? What position has each been given? Do you see just a trace of blue around the edge of the radio button that you can see?

If you click on the Good button, once again the display jumps back immediately to showing Hi.

Just a hint

Did you notice that the frontmost radio button beside Well appears checked. Does that help you answer the previous question?

There’s still no reverse animation. You’ll need to add a new rule for #good:checked.

body:has(#good:checked) {
  span::before {
    animation: letter-3 2.0s linear reverse forwards;
  } 
  span::after { 
    animation: letter-4 1.6s linear reverse forwards;
  }
}

The animation should play in reverse.

The Good radio button checked, reverse animation complete

Kill the spare

It would be nice to make this radio button pair behave like a single toggle checkbox… but with a third state. When the page is first loaded, you should see a button named “Well”. When you click on it, its name should become “Good”. Or rather, the Well label should be replaced by the Good label.

The new rule below will hide whichever label contains a :checked radio button:

label:has(:checked) {
  display: none;
}
Solution

However, both buttons both appear when the page is first loaded, when neither of them has been checked yet. Can you think of a second selector for the rule above to apply to? It should say something like: “If there is a label containing an unchecked radio button followed by another label containing an unchecked radio button, hide the second label.”

You’ll need to use a sibling combinator, like ~ or + to select the second label.

Solution

Here’s my solution. Can you adapt this to work with your version?

label:has(input:not(:checked)) + label:has(input:not(:checked)),
label:has(:checked) {
  display: none;
}

Counting Clicks: Take 2

At the beginning, I hope you played with the Arithmetic in Pure CSS calculator. Amongst other things, this allows you to cycle through the digits from 0 to 9 and then round to 0 again.

One of the questions that I suggested you ask yourself was: “How do I cycle through all the numbers, using a single button?” Well, now it’s time to give you an answer.

In the last exercise, you saw how to use just two radio buttons to cycle through just two different states (Well and Good). Can you imagine how you could extend this technique to cycle through all the numbers from 0 to 9 and then start again from 0?

Not with animations

OK, so technically, this exercise should not be in the Animations section, because it doesn’t involve any animations, so it won’t help you to think of a solution that uses animations.

But I did. I went down the wrong path. I went a long way down the wrong path before I gave up. Only Firefox stayed with me all the way.

If you’re interested, you can read below the story of my failed attempt to use animations to solve this problem.

A failed attempt

When I first planned this part, I had imagined that I would be able to use animation and two radio buttons, one on top of the other. My plan was to start with an animation which would repeatedly cycle through the digits 0 to 9, but which started off paused at 0.

The animation would start running as soon as the top radio button was checked. When the animation reached the next keyframe, it would move the label for the other radio button in front of the :checked button, so the label for the :checked button would no longer generate a :hover pseudo-class. I would have a rule that would pause the animation if there was no :hover pseudo-class on the :checked button. My plan was that the animation would stop at the current number, and stay there until the radio button which was now at the front was clicked…

Fiendishly clever, I thought. But only Firefox agrees with me. Safari doesn’t update the :hover class until you move the mouse, which leads to chaos. And the Chrome CSS elves just shake their heads and say: “Whaaat!?”

fail

If you feel so inclined, you can try it for yourself in different browsers.

I have included this failed attempt for a good reason, and not just because it took a lot of effort to get it to work at all. All progress takes effort. The article you are reading now and the demos that accompany it took many days to write. As soon as you start writing articles to share your knowledge, you will know that it’s worth it. When you decide to explain something in detail, you discover that there are still many details that you are unsure of, and you have to make the effort to understand them before you can continue.

One of the lessons that I learned from this failure was that Safari, Chrome and Firefox all treat :hover in subtly different ways. It’s not good enough to have a good idea. You have to test early, test often, and test on all target platforms (Thank for that mantra, John Dowdell!)

To repeat my earlier question: Can you imagine how you could extend the label-and-radio-button technique that you have just seen, to cycle through all the numbers from 0 to 9 and then start again from 0?

It’s time for new HTML and CSS files. Perhaps you’d like to call this project “Counting to 9”.

Counting without counters

Here’s the HTML for the first solution that I succeeded with:

<input type="radio" name="d" id="d0" checked>
<input type="radio" name="d" id="d1">
<input type="radio" name="d" id="d2">
<input type="radio" name="d" id="d3">
<input type="radio" name="d" id="d4">
<input type="radio" name="d" id="d5">
<input type="radio" name="d" id="d6">
<input type="radio" name="d" id="d7">
<input type="radio" name="d" id="d8">
<input type="radio" name="d" id="d9">
<label for="d1">1</label>
<label for="d2">2</label>
<label for="d3">3</label>
<label for="d4">4</label>
<label for="d5">5</label>
<label for="d6">6</label>
<label for="d7">7</label>
<label for="d8">8</label>
<label for="d9">9</label>
<label for="d0">0</label>

With no CSS, this creates a line of 10 checkboxes, followed by the digits 1234567890 in the same order as on the keyboard.

A row of radio buttons followed by digits

Note, though that the first input is checked, and this is the input withid="d0". The input for 0 is at the beginning, but the <label for="d0"> is at the end. Try clicking on the numbers to see which radio button becomes selected.

Only two labels at a time

I want to show only two labels at any one time:

If I use position: absolute for all the labels, they will all appear one on top of each other, with the <label for="d0"> at the top of the pile, immediately under the mouse. I’ll make everything really big, so it’s easy to click on the numbers:

label {
  /* Cosmetic */
  --size: 100vmin;
  width: var(--size);
  height: var(--size);
  font-size: var(--size);
  text-align: center;

  /* Logical */
  position: absolute;
}
Absolutely positioned labels

I won’t show the /* Cosmetic */ CSS any more, but you might like to keep it in your project anyway.

I need to put the <label for="d1"> in front of the <label for="d0">. My trick is to:

Now, a click on what seems to be the 0 will in fact be a click on the1.

label {
<!-- Cosmetic lines skipped -->
  position: absolute;
  z-index: 1;
  display: none;
}

#d0:checked ~ [for=d0] {
  display: block;
  opacity: 1;
}

#d0:checked ~ [for=d1] {
  display: block;
  opacity: 0;
}

[for=d0] {
  z-index: 0;
}

#d9:checked ~ [for=d0] {
  z-index: 1
}
Troubleshooting

If you don’t see any numbers on your page, check that the first radio button on the left i selected.

With the CSS shown above, if you click on the 0, <input id="d0"> will no longer be checked, but <input id="d1"> will be checked instead. And the rest of the page will go blank.

I now want <label for="d1"> to be visible, with <label for="d2"> in front of it, but invisible. I can do this by adding an additional selector to each of the rulesets that set both display and opacity.

#d1:checked ~ [for=d1],
#d0:checked ~ [for=d0] {
  display: block;
  opacity: 1;
}
#d1:checked ~ [for=d2],
#d0:checked ~ [for=d1] {
  display: block;
  opacity: 0;
}

Indeed, I can apply the same logic all the way up to 9.

label {
  position: absolute;
  z-index: 1;
  display: none;
}

#d9:checked ~ [for=d9],
#d8:checked ~ [for=d8],
#d7:checked ~ [for=d7],
#d6:checked ~ [for=d6],
#d5:checked ~ [for=d5],
#d4:checked ~ [for=d4],
#d3:checked ~ [for=d3],
#d2:checked ~ [for=d2],
#d1:checked ~ [for=d1], 
#d0:checked ~ [for=d0] {
  display: block;
  opacity: 1;
}
#d9:checked ~ [for=d0],
#d8:checked ~ [for=d9],
#d7:checked ~ [for=d8],
#d6:checked ~ [for=d7],
#d5:checked ~ [for=d6],
#d4:checked ~ [for=d5],
#d3:checked ~ [for=d4],
#d2:checked ~ [for=d3],
#d1:checked ~ [for=d2], 
#d0:checked ~ [for=d1] {
  display: block;
  opacity: 0.1;
}

[for=d0] {
  z-index: 0;
}

When I click on a digit, the next digit becomes fully opaque, and shows the label for its next digit very transparently in front of it.

This works until I click on the 8 and the label for 9 becomes fully opaque. But there is no sign of a semi-transparent 0.

I can follow what is happening in the Developer Tools Inspector:

Using the browser’s Inspector

When I get to 9, I want the next click to apply to the <label for="d0">. In the Inspector, this label appears to be in a higher layer than the <label for="d9">, but just now, I set the z-index of every label except the <label for="d0"> to 1, so that I could click on the <label for="d1"> instead. Now I’ll need one last rule to fix this for this one specific case:

#d9:checked ~ [for=d0] {
  z-index: 1
}

Try it! You should be able to cycle through all the numbers.

Detecting which number is currently checkd

This system has some advantages. At any time, I can use a selector which contains (say) #d3:checked, to check if the player is currently looking at a 3. This makes it easy to apply other rules to other elements when you select the answer that I want you to select.

Plus and minus

A major disadvantage is that you must click on the current number itself if you want it to increase. This is not an intuitive action.

It would be better to have a specific button that says +. And if you add a + button, why not add a button, too?

But, wait a minute… How can you make two buttons that work in opposite directions?

A Label with Two Wings

The task now is to create buttons Plus and Minus buttons that allow you to cycle through all the numbers.

Keeping it small

Whenever I am working in a big project and I come across a new problem, I create a new baby project to test my ideas and make all the mistakes I need to find a solution to this new problem. When I finally understand what I am doing, then I go back to the big project and apply my new solution.

Treating a new problem in isolation frees up your mind and makes it much easier to think in new ways.

So. New problem. New HTML and CSS files. You know the drill. “Plus and Minus” might be a good name for your project folder.

Preview

Here’s how the page you are about to create should work, but you can just create a proof-of-concept which doesn’t have all the pretty styling.

Here’s some HTML to get you started.

  <label class="d0">
    <span class="minus">–</span>
    <input type="radio" name="d" id="d0" checked>
    <span class="plus">+</span>
  </label>
  <label class="d1">
    <span class="minus">–</span>
    <input type="radio" name="d" id="d1">
    <span class="plus">+</span>
  </label>
  <label class="d2">
    <span class="minus">–</span>
    <input type="radio" name="d" id="d2">
    <span class="plus">+</span>
  </label>
  <!-- Something is missing here -->
  <span class="display"></span>
Reading the code

Take a good look at it and see if you can make sense of all the labels, inputs and spans. Why does each label have two spans? Can you see how this will generate something that looks a little like the Preview above?

Here’s how the HTML looks in a browser.

What the HTML shows

This short extract only gives you three labels. They have radio buttons with the ids d0, d1 and d9, but this is enough for you to check if this creates some kind of cycle. You can add more labels with the same structure later.

Each label has two spans: one with the class "minus", one with the class "plus".

Showing the right spans

Suppose <input id="d1"> is checked.

Can you write a CSS rule that will show only:

  • The .minus span for inside the <label class="d0">
  • The .plus span for inside the <label class="d2">?

Note that if any child of <label class="dX"> is clicked, the associated <input id="dX"> will become checked.

Solution

Here is the strict minimum that you need:

label span {
  display: none;
}
body:has(#d1:checked) {
  .d0 span.minus { display: block }
  .d2 span.plus { display: block }
}

**Note that I have used label span for the first selector, and not just span. because I don’t want to hide span.display.

This is what it will look like if you check the checkbox for #d1:

input#d1 is checked. Visible: the minus span for .d0, the plus span for .d2

If you click either the or the +, a different checkbox will be checked, and the and the + will disappear.

Not just cosmetic

I’m going to suggest, as a hint that you can use later, that you make the - and + elements bigger.

span {
  --size: 64px;
  font-size: var(--size);
}

Showing the value

The HTML I gave you includes this: <span class="display"></span>, but nothing is displayed in this span yet. I think you can guess that it’s meant to display a digit.

Perhaps, if you think about it, this digit could a counter used in a content declaration?

You could add a line to the ruleset for the #d1:checked button, and a new rule for the span.display:

body:has(#d1:checked) {
  counter-set: digit 1;
  .d0 span.m { display: block }
  .d2 span.p { display: block }
}

span {
  --size: 64px;
  font-size: var(--size);
}

span.display::after {
  content: counter(digit)
}

This would work. It would definitely work. But there’s a problem.

CSS won’t let you read the value of a counter, except to display it in a ::before or ::after element. So if you want to use the value in actual calculations, you’ll need to think of something else.

A different solution?

Is there a different CSS property that you could set… and then display the value of this different property?

Hint: how did I set the size of the spans that show the and + buttons?

How about this:

body {
  /* Declare a custom property and use it to set a counter */
  --counter: 0;
  counter-set: digit var(--counter);
}

label span {
  display: none;
}

body:has(#d1:checked) {
  /* Set the custom property to a new value */
  --counter: 1;
  .d0 span.minus { display: block }
  .d2 span.plus { display: block }
}

Custom properties have the advantage that they can be read from any child of the element in which they are declared. They can even be read by JavaScript. Try this in the Developer Console:

getComputedStyle(document.body).getPropertyValue("--counter")
getPropertyValue(“–counter”)
JavaScript in custom CSS properties

And if you are really sneaky, you can even store JavaScript code inside a custom CSS property. The official specifications for Custom Property Value Syntax explicitly requires browsers to make this possible. ( See “EXAMPLE 7”)

The trick

You can’t show the value of a custom property directly, but if it is a number, then you can use it with counter-set, and then get a counter to display it for you. You can’t read the value of a counter, but you can read the value of a custom property. Win-win.

Repeating a Pattern

You want to be able to cycle through the numbers 0 to 9, but for now, you can only show the number 1 (if the right radio button is checked). You need more labels with spans and radio buttons in your HTML page.

Apply the pattern

For brevity, I’m not going to list them all. You should be able to see the pattern and the missing labels.

  <label class="d0">
    <span class="minus">–</span>
    <input type="radio" name="d" id="d0" checked>
    <span class="plus">+</span>
  </label>
  <label class="d1">
    <span class="minus">–</span>
    <input type="radio" name="d" id="d1">
    <span class="plus">+</span>
  </label>
 <label class="d2">
    <span class="minus">–</span>
    <input type="radio" name="d" id="d2">
    <span class="plus">+</span>
  </label>
  <!-- Add a label for 3 with the appropriate class and id -->
  <label class="d3">
    <span class="minus">–</span>
    <input type="radio" name="d" id="d3">
    <span class="plus">+</span>
  </label>
  <!-- Continue the sequence up to 9 (4-8 are skipped here) -->
  <label class="d9">
    <span class="minus">–</span>
    <input type="radio" name="d" id="d9">
    <span class="plus">+</span>
  </label>
  <span class="display"></span>

More CSS

You’l also need more CSS rules, each with a similar pattern.

Cheating

In the CSS below, I’ve cheated to make the .d3 label jump directly to .d9, to match the HTML entries above and to keep the code short.

body {
  --counter: 0;
  counter-set: digit var(--counter);
}

label span {
  display: none;
}

body:has(#d0:checked) {
  --counter: 0;
  .d9 span.minus { display: block }
  /* Notice that the "minus" span belongs to 9 */
  .d1 span.plus  { display: block }
}
body:has(#d1:checked) {
  --counter: 1;
  .d0 span.minus { display: block }
  .d2 span.plus  { display: block }
}
body:has(#d2:checked) {
  --counter: 2;
  .d1 span.minus { display: block }
  .d3 span.plus  { display: block }
}
body:has(#d3:checked) {
  --counter: 3;
  .d2 span.minus { display: block }
  /* I've cheated: I used .d9 instead of .d4 */
  .d9 span.plus  { display: block }
}
/* And so on, up to 9 ... */
body:has(#d9:checked) {
  --counter: 9;
  .d3 span.minus { display: block } /* <<< .d3/.d8 cheat */
  /* Notice that the "plus" span belongs to 0 */
  .d0 span.plus  { display: block }
}

span {
  --size: 64px;
  font-size: var(--size);
}

span.display::after {
  content: counter(digit);
}

I suggest that you add the following cosmetic rulesets, so the - and + spans don’t keep changing places.

span {
  position: absolute;
  top: 30px;
}
span:first-child {
  left: 10px;
}
span:last-child {
  left: 60px;
}

Make these changes to your project, and then check if the plus and minus buttons work as expected.

Cycling from 9 to 0

Did you notice that the “plus” for 9 and the “minus” for 0 are different? That’s what makes the system cycle.

No repeat loops

This exercise demonstrates one of the frustrations of writing CSS-only activities. There are no repeat loops or Boolean logic in CSS. As a result, every pattern has to be written out methodically, chunk by chunk, and every variant on the pattern needs to be manually edited.

If you are familiar with SASS, you can use its flow control features to generate patterns in CSS. You can use JavaScript in NodeJS to generate corresponding patterns for HTML.

Nonetheless, in many cases, your CSS-only activities will require many many lines of code, most of which you will have to write by hand.

Next step: making a calculator?

The technique that I have described here is exactly the same as the one I used to create the Arithmetic in Pure CSS activity that you tried out at the beginning.

It’s true that the span “buttons” are a little more discreet there. There’s also a little calculation that is done with the values of the custom CSS properties that hold the digit values, and a little trick to show NaN if you try to divide by 0. But you should be able to reverse engineer everything that I did there… and if you get stuck you can always visit the GitHub repo and see all the CSS for yourself.

Incrementing with :hover

Your browser generates a :hover pseudo-class for every element that is currently under the pointer. If the pointer is a mouse, then there will always be a :hover pseudo-class created somewhere while the mouse is over the page.

If the pointer is a finger or a stylus on a touch screen, than a :hover pseudo-class is created when there is the first actual contact with the screen… and then (on Chrome for Android, at least), the :hover pseudo-class remains, even though no-one is touching the screen.

Here’s the simplest “game” I could think of that illustrates my point, inspired by “The Floor is Lava”. On a computer screen, you have to move your mouse as the green safe space gets smaller and floats away. I haven’t found any way to check that touchscreen players doesn’t just simply lift their fingers and uncheck the checkbox. And yes: I’ve tried. If you can find a solution, please, send me a link to your articles so that I can learn from you.

In other words: if your game is intended for playing on touchscreens, using :hover as a major feature of your game is likely to cause you headaches. If you use it for inessential features, then you can have fun with it.

Also, even in a game with a mouse, a custom :hover feature will stop working as soon as the mouse moves over a button or other element that is not a child of the element whose :hover you are tracking. It can be tricky to get buttons and :hover to work together… but it can be tricky in a good way.

So, with those warnings out of the way, would you like to have some fun with :hover… and with counters?

For the last time (in this article at least), I’ll ask you to create a new HTML file with CSS file linked to it. Here’s what you’ll be making (except that yours will have a white background.):

Here’s some HTML…

<div title="one">
  <div title="two">
    <div title="three">
      <div title="four"></div>
    </div>
  </div>
</div>

… and some CSS:

div {
  --size: 15vmin;
  --line: 1vmin;
  --delta: calc(var(--size) + var(--line) * 2);

  position: relative;
  top: var(--delta);
  left: var(--delta);
  width: var(--size);
  height: var(--size);
  border: var(--line) solid grey;

  &:hover {
    color: red;
    border-color: orange;
    counter-increment: divs;
  }

  &::after {
    content: attr(title) " " counter(divs);
  }
}

Open your page in your browser and move your mouse over the different <div>s:

Hovering over nested elements

The nested <div> elements are clearly not overlapping. In my screenshot above, the mouse is clearly over div.three. And yet three of the <div> elements have an orange border. This must have been caused by the rule:

  &:hover {
    color: red;
    border-color: orange;
    counter-increment: divs;
  }

And the text of all of the elements is red. And yes, the CSS elves are busy with some counter-increment activity as well. There is no counter: divs 0 declaration, but they have understood that they need to behave as if the declaration were there.

As you move your mouse around, the elements which are given a :hover pseudo-class will change. On a touchscreen, you can tap different parts of the screen and see how the :hover effect remains even after you remove your finger.

Where does ::after go?

Notice that the ::after pseudo-element for the first three <div>s is shown below the <div>, but the ::after pseudo-element for the fourth <div> appears inside it. That’s because the first three have children. The ::after pseudo-element is shown after the children… or rather, after where the children would be if they had not been given a different relative position.

The border of the <div> under the mouse and all its parents goes orange, because hovering, like the credit for a child’s achievements, is claimed by its parents, all the way back to the first generation.

The text of all the elements goes red when any one of them is under the mouse, because color, like eye-colour and debt for humans, is inherited from an element’s parents.

How The Map Works

You’ve already seen the Map activity. Now that you have an understanding of how CSS counters and @counter-style works, you can make sense of the tricks I have used.

Counting nesting depth

The Map uses counters to detect how deeply nested the element immediately under the touch point is.

The relief pattern for the hill and the sea is created by nested <div> elements:

      <div class="hill-1 contour">
        <div class="hill-2">
          <div class="hill-3">
            <div class="hill-4">
              <div class="hill-5"></div>
            </div>
          </div>
        </div>
      </div>

      <div class="sea-1 contour">
        <div class="sea-2">
          <div class="sea-3">
            <div class="sea-4">
              <div class="sea-5"></div>
            </div>
          </div>
        </div>
      </div>

Suppose the mouse is over <div class="hill-4">. As you saw in the last section, four of the divs with a class whose name begins with hill will have a :hover pseudo-class: div class="hill-4"> itself and all its parents.

@counter-style for height or depth

I’ve created an @counter-style at-rule to display a string that represents a height or a depth for each of its values:

@counter-style relief {
  symbols: "100m" "200m" "300m" "400m" "500m";
}

I increment a relief counter, for each divs with a class whose name begins with hill which has a :hover class.

div[class|=hill]:hover {
  counter-increment: relief;
}
counter and @counter-style with the same name

The CSS elves are smart enough to distinguish between a counter and an @counter-style that have the same name, so it makes sense to use the same name for both, to indicate that they work together.

And I use a ::before pseudo-element on the <p> element centred at the top of the page to show the height. But I hide this ::before element…

p.relief::before {
  content: "Height: " counter(relief, relief);
  display: none;
}

…unless the lowest div.hill-1 element has :hover applied to it:

.hill-1:hover ~ p.relief::before {
  display: inline;
}

Dealing with the grid

The grid pattern is even more complex: ten vertical rectangular <div>s, one for each of the columns, all nested inside each other, and inside each column.

In this composite image below, you’ll see how the mouse is hovering over a <div class="row-3">, which is a child of all the following:

Hovering over cell B3

In the CSS, a counter called col is incremented for each div whose class name begins with col

div[class^=col]:hover {
  counter-increment: col;
}

… and a counter called row is incremented for each div whose class name begins with row

div[class^=row]:hover {
  counter-increment: row;
}

Each div whose class name begins with row also has its own ::before element, whose content is determined by the col and row counters. However, these ::before elements acquired not shown…

div[class^=row]::before {
  content: counter(col, upper-alpha) counter(row);
  position: absolute;
  width: 30px;
  line-height: 30px;
  text-align: center;
  display: none;
}

… except for the div which is immediately under the mouse:

/* Show a label for the hovered cell... */
div[class^=row]:hover::before {
  display: inline-block;
}
/* ...but not for all the cells in the parent rows */
div[class^=row]:has(:hover)::before {
  display: none;
}

The Heist

So here’s a challenge that I set for myself: How much can I do using just :hover as the trigger? How much can I do using no checkboxes, no radio buttons, no details elements.

Here’s what I came up with:

Each action is performed in a nested <div> and the nesting gets deeper as the story progresses. As a result, the innermost element with a :hoverpseudo-class maintains a :hover pseudo-class on all its parents. In this way, any changes made at one level continue to apply to the inner levels.

However, if you move your mouse off the animation, it will reset and the story will start over from the beginning.

On touchscreen devices, the innermost element with a :hover pseudo-class retains this class even after the contact with the touchscreen ceases. If a child <div> is activated by a touch on its parent, the child <div> does not immediately receive a :hover pseudo-class. (With a mouse, the child <div> will receive its pseudo-class as soon as the mouse moves, if not sooner.)

The numbered instructions at the top are set using a counter for the number of <div> which have a :hover pseudo-class. The text is set by an @counter-style at-rule.

Click on the GitHub octocat icon to visit the GitHub repository.

Enjoy!

Complete a Game by Adding Counters

I wrote at the beginning of this article that I would not be showing you, step by step, how to build a particular game. However, I do want to give you the chance to test whether my explanations have made good sense to you.

I’ve created a pure CSS game that uses the techniques that I have described above. You can find the GitHub Repository here.

Adding counters

If you clone the repository and launch the game on your own computer, you will find that the scoring and timing system is missing. Using the knowledge that you have just acquired, can you get your local version of the game to behave the same as the online version?

The logic of the game (starting and stopping it, picking up the gold tokens, showing the result when you reach the end) is already written. You’ll find the CSS for this in a file called styles.css. You should not need to change anything in this file or in the index.html file.

There is a second CSS file attached to the HTML page. It’s called counters.css, and it currently contains no CSS at all. You should now be able to write CSS of your own that:

  • Shows the score
  • Shows an emoji that gets happier as the score increases
  • Shows how many seconds have passed since the player clicked on the green Start button
  • Stops the timer when the game is over.

And of course, you can use this as the starting point for your own remixed version of the game. I’m sure that, artistically, your CSS power does exceed my own…

What I Hope You Know Now

If you have worked your way through all the explanations and the exercises up to here then you should have a clearer idea about:

  1. Using checkboxes and radio buttons to store state
  2. Using animations to change state
  3. Using ::before and ::after pseudo-elements
  4. Using z-index to reorder elements
  5. Using the :hover pseudo-class to intercept the actions of the mouse
  6. Writing selectors that control the logic
  7. Using custom CSS properties

Specifically, you should have a deeper understanding about how you can use and abuse CSS counters, @counter-styles, list-style-type and styling the ::marker pseudo-element for lists.

You can apply this new knowledge not just to creating CSS-only activities, but to the CSS that you use in any project.

Thinking outside the box

More importantly, I hope that you have picked up some ideas on how to explore a topic, how to test its limits, how to take it in new directions.

I’ve suggested that you have fun making mistakes and testing unorthodox approaches. Be sure, though, to test in all the major browsers as you go, so that you do not create something that only works in one browser when the stars are correctly aligned.

Introduction

CSS is not a programming language. Not officially. But it is possible to create games with only HTML and CSS. And there are even some tutorials on how to make a specific game: a click ’em all, a jigsaw, a maze, and maybe more that I have not found.

My aim here is different. In this series of articles, I want to get you to experiment with the different techniques that you can use to create your own games. By the end of each article, you won’t have created a game, but you will have understood why some things work and some things don’t.

In this article, you’ll be looking at the following ideas:

  1. Using checkboxes and radio buttons to store state
  2. Using animations to change state
  3. Using ::before and ::after pseudo-elements
  4. Using z-index to reorder elements
  5. Using the :hover pseudo-class to intercept the actions of the mouse
  6. Writing selectors that control the logic
  7. Using custom CSS properties
  8. Working with counters

My main focus here will be on the last two ideas: understanding how CSS custom properties and counters work together. You need to use counters, after all, to number things: to show the score or to display a timer, for example. These are simple ways to add urgency and excitement to a game. And if you think laterally, you can also get counters to do some quite unexpected things for you, as you will see.

I won’t treat the other seven ideas is such great depth here. I’ll be writing other articles about them specifically.