Building a CSS game without HTML or JS
Tags: css, html, webdev, tutorial
Published: 2020-12-28 21:49:21

---
title: Building a CSS game without HTML or JS
published: true
description: Article explaining how to create a small video game with CSS without using JS or HTML (or kind of).
tags: css,html,webdev,tutorial
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/9pc2t7epgu5ey030jmdk.png
---

It all started with a [demo from @jheyy](https://codepen.io/jh3y/pen/rNLqRvZ) (that for some reason doesn't seem to work on my computer anymore) in which without using a single HTML element, he created a Mario Cart animation... and then a tweet:

{% twitter 1326926202729361408 %}

I was excited about the challenge and hate to admit that I tried to change JHey's code but couldn't fully untangle it. I got it to do some things, but not much... so I decided to do something independently. It wouldn't be as fancy, but it would make it work.

The [result is on this CodePen](https://codepen.io/alvaromontoro/pen/PoGJjQe). And you can watch a [demo on Youtube](https://www.youtube.com/watch?v=Mg8_nM7Zfwc):

> NOTE: I just linked the demo instead of embedding it on the article because WebKit browsers (Chrome, Safari, Brave, new Edge, etc.) seem to have issues with large animated and tilted elements and causes browsers to slow down or flash colors. **The demo will work better on Firefox**.

{% youtube Mg8_nM7Zfwc %}

<figcaption>There's more cool CSS videos on the channel, <a href="https://www.youtube.com/user/alvaromontoro?sub_confirmation=1">subscribe now!</a></figcaption>


The game actually works (with some quirks, as we'll see later.) And, as it can be seen in the linked Codepen, it is composed of:

- **200 lines of CSS** (including comments).
- **0 lines of JavaScript**.
- **0 lines of HTML**.

**The zero lines of HTML are just an illusion**: there's still a basic HTML structure with the `<html>` and `<body>` tags (as we [explain in this other article](https://dev.to/alvaromontoro/drawing-a-snowman-in-css-with-no-html-elements-57g7))... and we'll use them to generate the game.

What is this witchcraft? How was it done? Let's see it.

But before that, let me say that this is a fun approach to Web Development and CSS, and it's great for practicing and learning, but it has a little-to-none practical outcome. The result is a nice demo, but never try something like this on a production site.

## How it was done

The article will contain explanations with plenty of details, but if you want to see a video of the process, you can watch this on [my Youtube channel](https://www.youtube.com/user/alvaromontoro):

{% youtube XcavnNrgF3c %}

<figcaption>The video at 20x only lasts for 7 minutes</figcaption>

Even when we have only two elements (`<html>` and `<body>`), we can style six if we count the `::before` and `::after` pseudo-elements for each of them.

This game only uses 5 of the six.

### The `html`: background and cursor

The styles for the `html` will include three important parts:

1. The basic game background with a blue sky and green ground.
2. A customized cursor that looks like a car.
3. Perspective, so some elements look 3-dimensional (we'll see this later).

The **background can be achieved with a [`linear-gradient`](https://developer.mozilla.org/en-US/docs/Web/CSS/linear-gradient())**. Nothing fancy for the sky because it will be covered by another element later. The ground will also include some other [`radial-gradient`](https://developer.mozilla.org/en-US/docs/Web/CSS/radial-gradient())s so it's not just plain green but has different tones and shapes.

**The customized cursor may be a bit more interesting**. In CSS, we can specify which cursor will be used with the [`cursor` property](https://developer.mozilla.org/en-US/docs/Web/CSS/cursor). We can pick among a list of system cursors or set an external image using `url()`.

We wanted to have all the code encapsulated in the CSS, so we opted for an inline SVG instead of an external image file. You can [add an inline SVG using the `data:` protocol](https://stackoverflow.com/a/10768631/3695983) like this:

```css
html {
  cursor: url(data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'>...</svg>), auto;
}
```
Two key things when specifying a custom cursor:

1. You must always include a system fallback. Otherwise, the browser will not display the image as the cursor.
2. The inline SVG must include the `xmlns` attribute, or the SVG will not be rendered.

For the inline SVG, we used rectangles and paths to simulate a small car. The result is simple but catchy:

<figure>
![Cartoon of a polygonal car seen from the back](https://dev-to-uploads.s3.amazonaws.com/i/xqpgy3e669rhf720ph5u.png)
<figcaption>Not perfect, but no bad for one line of CSS/SVG</figcaption>
</figure>

One last detail for the car: the top-left corner of the car is the actual pointer; this is not natural because you can go on top of one of the obstacles and still not lose. 

The `cursor` property allows setting a coordinate of where the pointer will be within the cursor. The value has to be smaller than 32, though. So we change the CSS accordingly:

```css
html {
  cursor: url(data:image/svg+xml;utf8,...) 31 31, pointer;
}
```

### `html::before`: the road

Thanks to the perspective in the `html` –we said we would see it later–, we can create a gray rectangle with the `::before` pseudo-element, and apply a [3D rotation](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/rotate3d()) to make it look like the road is fading away, getting smaller in the distance.

The tricky part with this one was the size. Initially, the height was 100%, but it fell short of the horizon's edge after the rotation. We had to adjust the height to make them match manually.

> NOTE: there's a mathematical formula to calculate the exact height. But it was faster a try-error that got a close-enough value within seconds.

To paint the road, we use two linear-gradients:

- One horizontal for the paved area (transparent-gray-transparent).
- One vertical for the red and white shoulder lines.

Finally, we create a small [animation](https://developer.mozilla.org/en-US/docs/Web/CSS/animation) to make the background move from top to bottom, so it simulates the car is moving. Talk about relativity! The car stays in the same place; it's the whole world that moves under its tires.

<figure>
```scss
@keyframes moveRoad {
  0% { background-position: 0 0; }
  100% { background-position: 0 16.3vh; }
}
```
<figcaption>The value 16.3vh is because the road is 163vh and there are 10 sets of red-white paint.</figcaption>
</figure>

We later added some shadows and blur to decorate the road a little. Nothing fancy, though.

### `html::after`: the sky

`html`'s `::after` pseudo-element is used to draw the sky and some mountains on the horizon, but it also serves a fundamental purpose: **hide the overflow of the body**.

As we will see in the next section, the `body` "overflows" its container, and it may look like it flies into the sky. To avoid that, we place the sky on top of it. Then we decorate it with the sun, some mountains, a tree or two... and it looks like a charm.

We used a' radial-gradient' to generate the sun and the rounded mountains; For the mountain and the tree, we used `linear-gradient`. Then played a bit with the background sizes and positions to set everything in place.

<figure>
![Screenshot of the sky in the game with the word linear-gradient with arrows pointing to a mountain and a tree trunk; and the word radial-gradient with arrows pointing to the sun, some rounded mountains, and the treetop.](https://dev-to-uploads.s3.amazonaws.com/i/ntawcdtuo49wmjexeu57.png)
<figcaption>The different gradients used to generate the horizon and sky</figcaption>
</figure>

### `body`: the fun begins

This is the tricky part, one that, if you see the video, got me hitting my head against the desk for a little bit.

The idea is to use the body to create a path that the car must follow to "win the game" (the game never ends, so the only option is to lose eventually).

First, we start by making it a REALLY tall element and tilting it at the same angle that we rotated the road; that way, it will overlap it.

> AUTHOR'S NOTE: **Here is where my problems began**. I was coding on a WebKit browser, and it started struggling with the large tilted elements. Especially after cropping and animating them. At this point, **I changed my development browser to Firefox**, which seemed to do just fine. 

We can create this by cropping the body using [`clip-path`](https://developer.mozilla.org/en-US/docs/Web/CSS/clip-path). We only keep some parts of the track available with a zig-zagging path while removing/clipping the rest.

Now the car/mouse can move from right-to-left (and vice versa) on top of the `body` but without mousing over it, because it has been clipped—time to draw some grease stains with `radial-gradient`.

<figure>
![Screenshot of the game without the sky, the grease stains on the road fly beyond the sky](https://dev-to-uploads.s3.amazonaws.com/i/p07r7isyetrv644zhzfh.png)
<figcaption>Without the <code>html::after</code>, the road would be visible over the horizon</figcaption>
</figure>

We then add animation, so the road/obstacles move from top to bottom. We can do that using the [`translate` transform](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/translate()) (no need for 3Ds now).

The only thing left to do would be to stop the game when the car/mouse goes on top of the grease stains. And for that, we still have two pseudo-elements left... but we only need one.

### `body::after`: the end screen

The `body::after` pseudo-element will be a Game Over screen. It will be invisible by default (`display: none`) and only show up when the mouse goes over the `body` (aka, when the car goes on top of a grease stain).

We will also have to style the `body`. In this case, it is important that we:

- Remove the `clip-path`. Or the game over message will be cropped too!
- Adjust the height to the viewport. Instead of being a really tall container.
- Remove the 3D rotation. Or the game over message will be tilted too!
- Remove the animation. So the message doesn't just slide out the viewport.
- Add a z-index. So the `body` goes on top of the `html::after`.

We can do all that by resetting styles with a couple of rules:

```scss
body:hover {
  transform: none;
  width: 100vw;
  height: 100vh;
  top: 0;
  left: 0;
  z-index: 1;
  clip-path: none;
  animation: none;
  background: none;
}

body:hover::after {
  display: flex;
}
```

We used [Flexbox](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout/Basic_Concepts_of_Flexbox) for the `body::after` so we could easily center text with [`align-items`](https://developer.mozilla.org/en-US/docs/Web/CSS/align-items) and [`justify-content`](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content).

One trick to add multiline content in a `::before`/`::after` pseudo-element is putting a `\a` wherever the new line should go, and then adding a [`white-space: pre`](https://developer.mozilla.org/en-US/docs/Web/CSS/white-space) so the blank spaces are preserved:

```scss
body::after {
  content: "GAME OVER \a\a Move the mouse \a outside of the window \a to restart the game";
  white-space: pre;
  /* ... */
}
```


## Extra features and concerns

### Custom settings

One cool thing about this game –or at least, *I* think it's cool–, is that it can be easily configured.

By replacing some values with [CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties), we can adjust different animation and game settings by updating a single line of code. Seriously, isn't that cool?

In this game, we added three settings:

- Speed: it is the [time in seconds of the animation](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-duration) that moves the grease stains from top to bottom. Lower numbers will make the game faster (and more difficult).
- Timing: the [animation timing function](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function). By default, it is "linear," which means that the animation is at the same speed all the time. We could change it to any value and make the game move at different speeds, making it more difficult.
- Animation: the [animation iteration count](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-iteration-count). By default, it is "infinite," which means that the game will run continuously, but you can change the value to any number... Not the best variable name tbh.

> AUTHOR'S NOTE: I tried adding a fourth CSS variable for the player to be able to pick a high/low-resolution version of the game (it plays nicer on smaller screens or with low-resolution). But the result was not as expected... One improvement to add later.

### The `:hover` problem

For the player to lose, they must place the car/mouse over a grease stain... but there's a problem: **the `:hover` state is only triggered if the mouse is moving**. 

If the player places the car in the middle of the road and then releases the mouse, the browser will not trigger the `:hover` effect, and the game will never end.

<figure>
![Screenshot of the game with the car moving on top of a grease stain and no game over message](https://dev-to-uploads.s3.amazonaws.com/i/8qpidmfmtpemf6pxh22y.png)
<figcaption>The game can be tricked by not moving the mouse</figcaption>
</figure>

This behavior makes sense from a security point of view: we don't want to trigger a `:hover` side effect unintentionally (e.g., without any interaction from the user)... it is just a bit unfortunate for this game ([and for this one too](https://codepen.io/alvaromontoro/full/eYYzzWg)).

Also, there's another catch. Because of the perspective, the `body` "shrinks" as it moves up the screen. This means that the page's sides are not covered by the `body` and could be used to trick the game.

A possible solution for this would be to make the width of the `body` much larger, say `200vw` instead of `100vw`. That would solve the problem but would make the animation struggle and lag.

An option to make it a bit more performant would be using actual images for the backgrounds and image stains. The browser would not need to compute as much, and the animation should be smoother.

## Final thoughts

It was a lot of fun to develop this game. The coding took around two and a half hours ([the video above is at 20x](https://www.youtube.com/watch?v=XcavnNrgF3c), so it lasts only 7 minutes), and required some additional time for planning.

As we mentioned above, this is not really practical, although it is a great way to learn and explore CSS.

It is a pity that it doesn't work smoothly on WebKit browsers (they are the most extended ones at the moment). A new challenge will be to develop a second version without so many 3D rotations and animations, which should make it nicer for those browsers.