CSS Pixel Art Explained

Pixelated eyeball drawn in pure CSS

There are endless ways to create pixel art including the use of image editors like Photoshop, and it’s also possible in CSS! In this post, I will show you how to create CSS pixel art. Some notable examples include these Super Mario Bros. sprites by Una Kravets and this animated bat by Tim Guo.

We’ll use a grid to assist in locating spaces for pixels and placing them. Then, we’ll draw something as easy as a short line just to understand how it works. Then as a more complete yet simple example for this post, I’ll break down some code for a small eyeball I created above.

Setting up the Sprite Grid

For the HTML, let’s set up a grid div with a sprite div inside it. This is where the line will be rendered.

<div class="grid">
  <div class="sprite"></div>
</div>

For the CSS, we’ll use Sass (SCSS) for making changes to the grid and sprite easy, and you will see why this will save time. If you aren’t already familiar with Sass, you can check out an introduction on the language’s own site.

We’ll use the variables for controlling the size of one sprite pixel using CSS pixels, the color of the grid lines, a color array (for now, only one color), and for the actual sprite code, which is going to be composed of a chain of box shadows, but we’ll cover that later on.

$pixel: 10px;
$gridColor: #888;
$color: #000;
$sprite: 0; // to be defined later

To draw the grid, we’ll use two linear gradients for the horizontal and vertical lines. To make them repeat, we use a background size with the dimensions of one sprite pixel. The width and height of the grid will be determined by multiplying $pixel by the number of spaces we want both horizontally and vertically (16 × 16 spaces in this example).

.grid {
  background-image:
    linear-gradient(transparent $pixel - 1, $gridColor $pixel, $gridColor),
    linear-gradient(90deg, transparent $pixel - 1, $gridColor $pixel, $gridColor);
  background-size: $pixel $pixel;
  border: 1px solid $gridColor;
  overflow: hidden;
  width: $pixel * 16;
  height: $pixel * 16;
}

And this is how the grid should appear as a result:

Empty sprite grid

Then, we give .sprite both the width and height the value of $pixel. $sprite may not be defined yet, but we’ll do so in the next section.

.sprite {
  width: $pixel;
  height: $pixel;
  box-shadow: $sprite;
}

Drawing a Short Line on the Grid

To draw a simple 1×2 line, we’ll start at the top left corner of the grid and then see the output. Then, we go back to the $sprite variable and add the following code:

$sprite:
  $pixel $pixel nth($color, 1),
  ($pixel*2) $pixel nth($color, 1);

Here, the box shadow parameters we just used for each pixel are the X position, Y position, and color of the sprite pixel (#000 for nth($color, 1)). The blur and spread values that would go before the color are not needed, as they are 0 by default. Since the value of $pixel is 10 CSS pixels, we placed the first sprite pixel one increment of 10 CSS pixels to the right and one increment of 10 CSS pixels downward. We placed the second pixel after the first by shifting it two squares horizontally by multiplying the X position $pixel by 2.

Now notice the issue with the compiled CSS result. The top left corner of the line we just drew is not quite positioned at the top left corner of the grid.

Pixel placement on grid
Note: Figure enlarged and cropped for focus

To fix that, we use the transform property to translate the sprite upwards and to the left by the value of $pixel. The overflow: hidden; in the grid will hide the contained sprite div so that only the box shadows are visible.

.sprite {
  width: $pixel;
  height: $pixel;
  transform: translate(-$pixel, -$pixel);
  box-shadow: $sprite;
}

Corrected sprite placement
Note: Figure enlarged and cropped for focus

Scaling

If we decided to change the value of $pixel, everything will be instantly scaled to it. This eliminates the need to adjust every box shadow manually if coded in regular CSS. Consider the regular CSS version the .sprite class below. If you wanted to use a different pixel size or change one color shared by multiple pixels, it would be long, tiresome, and a waste of time to adjust every little value accordingly!

.sprite {
  width: 10px;
  height: 10px;
  transform: translate(-10px, -10px);
  box-shadow: 10px 10px #000, 10px 20px #000;
}

Drawing the Eyeball

Now, here is the Sass code with $sprite storing box shadows for first two lines of the eyeball, and I included the color variables as well:

$pixel: 10px;
$gridColor: #888;
$color: #000 #aaa #ddd #048 #07b #fff #800 #c00 #c88;
$sprite:
  ($pixel * 6) $pixel nth($color, 1),
  ($pixel * 7) $pixel nth($color, 1),
  ($pixel * 8) $pixel nth($color, 1),
  ($pixel * 9) $pixel nth($color, 1),
  ($pixel * 10) $pixel nth($color, 1),
  ($pixel * 11) $pixel nth($color, 1),
  ($pixel * 4) ($pixel * 2) nth($color, 1),
  ($pixel * 5) ($pixel * 2) nth($color, 1),
  ($pixel * 6) ($pixel * 2) nth($color, 8),
  ($pixel * 7) ($pixel * 2) nth($color, 6),
  ($pixel * 8) ($pixel * 2) nth($color, 6),
  ($pixel * 9) ($pixel * 2) nth($color, 8),
  ($pixel * 10) ($pixel * 2) nth($color, 6),
  ($pixel * 11) ($pixel * 2) nth($color, 9),
  ($pixel * 12) ($pixel * 2) nth($color, 1),
  ($pixel * 13) ($pixel * 2) nth($color, 1);

First two rows of eyeball

Box shadows that form the second row of pixels always use ($pixel * 2) as the Y position, meaning that the Y position $pixel of box shadows in subsequent rows will be multiplied by the row they are on ($pixel * 3 for pixel in 3rd row, $pixel * 4 for pixel in 4th row, etc.).

We can better organize these rows of pixels by putting each group of box shadows in variables called $row1 and $row2. Then for the $sprite variable, we would need to combine the $row* variables as shown below.

$row1:
  ($pixel * 6) $pixel nth($color, 1),
  ($pixel * 7) $pixel nth($color, 1),
  ($pixel * 8) $pixel nth($color, 1),
  ($pixel * 9) $pixel nth($color, 1),
  ($pixel * 10) $pixel nth($color, 1),
  ($pixel * 11) $pixel nth($color, 1);

$row2:
  ($pixel * 4) ($pixel * 2) nth($color, 1),
  ($pixel * 5) ($pixel * 2) nth($color, 1),
  ($pixel * 6) ($pixel * 2) nth($color, 8),
  ($pixel * 7) ($pixel * 2) nth($color, 6),
  ($pixel * 8) ($pixel * 2) nth($color, 6),
  ($pixel * 9) ($pixel * 2) nth($color, 8),
  ($pixel * 10) ($pixel * 2) nth($color, 6),
  ($pixel * 11) ($pixel * 2) nth($color, 9),
  ($pixel * 12) ($pixel * 2) nth($color, 1),
  ($pixel * 13) ($pixel * 2) nth($color, 1);

$sprite: $row1, $row2;

After adding the rest of the rows of pixels, the eyeball will look like this:

The completed eyeball

To hide the grid, you can simply go back to the .grid class and remove or comment out the background-image, background-size, and border properties.

.grid {
  /*
    background-image:
      linear-gradient(transparent $pixel - 1, $gridColor $pixel, $gridColor),
      linear-gradient(90deg, transparent $pixel - 1, $gridColor $pixel, $gridColor);
    background-size: $pixel $pixel;
    border: 1px solid $gridColor;
  */
  overflow: hidden;
  width: $pixel * 16;
  height: $pixel * 16;
}

Conclusion

In the long run, you can create CSS pixel art by using a mass combination of box shadows and configure each box shadow using this syntax: (size of pixel × X position), (size of pixel × Y position), pixel color. Since drawing every single pixel may be a tedious process, you can speed it up using a generator like Shmoop software engineer Daniel Nieh’s. All I wanted to cover here is how the creation and placement of pixels work. You may see some differences in other approaches, but note that they often employ box shadows just like what I did in my example. I have the full source code of the eyeball available on CodePen for your reference.


Posted in: CSS