πŸ’‘ This page contain affiliate links. By making a purchase through them, we may earn a commission at no extra cost to you.
How to Make Pixel Art With Box Shadows

How to Make Pixel Art With Box Shadows

Generate pixel arts with the help of Sass
Ferenc Almasi β€’ Last updated 2021 November 11 β€’ Read time 13 min read
Pixel art, one of the earliest forms of digital art, where images are created down at the pixel level. Learn how you can use CSS to create them.
  • twitter
  • facebook
CSS

Pixel art, one of the earliest forms of digital art, where images are created down at the pixel level. Nowadays, it’s not something you can see on a daily basis unless you are into classic video games or pixel art itself.

In this tutorial, we will bring back pixel art to life and we will try to push the limits of CSS to use it to recreate Mario from the famous Super Mario Bros. game. On top of that, we are only going to use a single HTML element for that.


Pixel Art and Box Shadows

As you could already guess from the title, we are going to achieve this using nothing more but CSS box-shadows. So how come the box-shadow property can generate pixel arts?

This is because, without a spread and blur value, you can replicate the shape of the original element in every possible direction. The box-shadow property also has the advantage, that you can create as many shadows as you want by comma separating them.

Drawing a pixel with box shadow

Copied to clipboard! Playground
.box-shadow {
    background: beige;
    width: 10px;
    height: 10px;
    box-shadow: 10px 0 red, 0 10px green;
}
box-shadow.css

The above rule will create a 10Γ—10 rectangle with one red rectangle next to it, and one below it:

We can make box-shadows as complicated as we want them to be, and even create pixel arts. And all you have to have is a single HTML element and a long list of box-shadow rules. However, it would be insane to calculate and write them out manually. For example, to recreate Mario standing from a 16x16px image, you would need 256 different box-shadow rules, exactly this much:

The amount of box-shadow needed for 256 pixels
This box-shadow contains 256 different rules

Generating Pixel Arts with Sass

To make things more manageable, we will use the help of Sass. For this project, all you will need is a single HTML element, let it be mario:

Copied to clipboard!
<div id="mario"></div>
index.html

And a Sass file that you can transpile down to CSS. For the sake of simplicity, I’m using this small script after I’ve installed Sass locally:

Copied to clipboard! Playground
const fs   = require('fs');
const sass = require('sass');

const res  = sass.renderSync({file: 'pixels.scss'});

fs.writeFileSync('pixels.css', res.css);

console.log('βœ… CSS file created');
parse.js

Inside the Sass file, we want to use a mixin like the one below to generate a pixel art based on some parameters:

Copied to clipboard!
#mario {
    position: absolute;
    top: 50%;
    left: 50%;
    @include pixel-art($pixelSize, $pixelMap, $colorMap);
}
pixels.scss

So what are the values of the variables? The $pixelSize simply defines the size of each pixel, while $pixelMap and $colorMap are used for looking up where and which color to use for each box-shadow:

Copied to clipboard!
$pixelSize: 10px;
$pixelMap: (
    (0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0)
    (0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0)
    (0 0 0 2 2 2 3 3 2 3 3 0 0 0 0 0)
    (0 0 2 3 2 3 3 3 2 3 3 3 3 0 0 0)
    (0 0 2 3 2 2 3 3 3 2 3 3 3 3 0 0)
    (0 0 2 2 3 3 3 3 2 2 2 2 2 0 0 0)
    (0 0 0 0 3 3 3 3 3 3 3 3 0 0 0 0)
    (0 0 0 2 2 1 2 2 2 2 2 0 0 0 0 0)
    (0 0 2 2 2 1 2 2 1 2 2 2 0 0 0 0)
    (0 2 2 2 2 1 1 1 1 2 2 2 2 0 0 0)
    (0 3 3 2 1 3 1 1 3 1 2 3 3 0 0 0)
    (0 3 3 3 1 1 1 1 1 1 3 3 3 0 0 0)
    (0 3 3 1 1 1 1 1 1 1 1 3 3 0 0 0)
    (0 0 0 1 1 1 1 0 1 1 1 1 0 0 0 0)
    (0 0 2 2 2 2 0 0 0 2 2 2 2 0 0 0)
    (0 2 2 2 2 2 0 0 0 2 2 2 2 2 0 0)
);
$colorMap: (
    0: transparent,
    1: #FF0000,
    2: #AC7C00,
    3: #FFA440
);
pixel.scss

What you see here, is a matrix, containing 16 lists, each containing 16 values. Each number corresponds to a color, that is defined in a map below the matrix. 0 stands for a transparent color, 1 is for red, and so on. If you look at the matrix from a distance, you can already see Mario taking shape:

Outline of Mario from the code editor
Outline of Mario

We want to loop through this matrix and generate the appropriate box-shadow values for every pixel. Let’s start with the very basics, the shape of the mixin:

Copied to clipboard!
@mixin pixel-art($size, $map, $colors) {
    $shadow: null;

    display: block;
    width: $size;
    height: $size;
    box-shadow: $shadow;
}
pixels.scss

This is what the mixin will generate in the very end. We’re going to populate the $shadow variable with the necessary values. For this to happen, we want to loop through each value, which means, we need two nested loops, one for each list, and one for each value inside the list:

Copied to clipboard!
$i: 1;
$j: 1;

@each $list in $map {
    @each $pixel in $list {

        $i: $i + 1;
    }

    $i: 1;
    $j: $j + 1;
}
pixels.scss

Unfortunately, @each, nor @for has the ability to get an index number from the loop, so we have to create and increment them manually. Make sure you reset $i after each $list. Inside the inner loop, we then want to generate a box-shadow for each value inside the matrix. We need two things:

  • The x and y coordinates of each box-shadow
  • The color associated with it

Based on the size of the pixel and the index of each value inside the matrix, we can calculate where to put the box-shadow, by simply multiplying the size with one of our counters. For the color, we can use map.get to get the appropriate value from $colorMap based on the value inside the matrix:

Copied to clipboard!
@use 'sass:map';

$x: $i * $size;
$y: $j * $size;
$color: map.get($colors, $pixel);
pixels.scss

Don’t forget you need to include the Sass module at the top of your file. Previously, these functions were globally available, they still are to this day, but their use is discouraged as eventually, they will be phased out. We can then add these values together and add it to the $shadow variable:

Copied to clipboard!
$shadow: $shadow + #{' '}              // Original value + empty whitespace
                 + $x + #{' '} + $y    // New x,y coordinates
                 + #{' '} + $color;    // Plus the color
pixels.scss

Note that we are using interpolation. If you were to simply append white spaces to the values, you would get a string back, which would make the box-shadow rule invalid. In any case, you could still use string.unquote to convert the string back to a valid box-shadow value.

This implementation so far, however, will not work. This is because we need to comma separate each value. But to do that, we have to wrap the logic into an @if statement:

Copied to clipboard!
@if not ($i == list.length($list) and $j == list.length($map)) {
    $shadow: $shadow + #{','};
}
pixels.scss

This will make sure we only separate the rules by commas if we are not at the very last element. Otherwise, we would get a comma at the end of the box-shadow value, which again, invalidates the rule. If you did everything right, you should get the following pixel art in your document:

However, since the whole box-shadow is calculated from the top-left corner, we may not get the correct position we are looking for. Let’s make the anchor point to be centered instead. For this to work, we can define two additional variables inside the mixin:

Copied to clipboard!
@use 'sass:list';

$translateX: 0;
$translateY: -(list.length($map) * $size) / 2;
pixels.scss
Don't forget to include the list module at the top of your Sass file

One for the x, and one for the y position. The y position can be calculated right away. We need to get the length of the matrix times the size of pixels and divide it by two. Inside the inner @each loop, we can also calculate the x position using the same logic, but using the length of one of the lists this time:

Copied to clipboard!
@if ($translateX == 0) {
    $translateX: -(list.length($list) * $size) / 2;
}

...

transform: translate($translateX, $translateY);
pixels.scss

Then all you have to do, is pass these values into a translate function, and now your pixels are centered. But let’s not stop here, we can also switch between different box-shadows that would create an animation right? Right!

Anchor set to top left vs center
Looking to improve your skills? Master CSS from start to finish.
Master CSSinfo Remove ads

Animating Pixel Arts

For this, let’s create a separate mixin called animate-pixel-art:

Copied to clipboard!
@include animate-pixel-art($pixelSize, $frames, $colorMap, $fps);
pixels.scss

Unlike the previous mixin, this can take in a frames map and an FPS value. Let FPS be 5, and $frames be a map that contains a matrix for each frame:

Copied to clipboard!
$frames: (
    0: ( ...first frame ),
    1: ( ...second frame ),
    2: ( ...third frame ),
);
pixels.scss

This means we need to change a couple of things around for the mixin. First of all, we will need a map inside the mixin where we can store the generated box-shadow value for each frame:

Copied to clipboard!
@use 'sass:string';

@mixin animate-pixel-art($size, $frames, $colors, $fps) {
    $shadows: ();
    $shadow: null;
    $i: 1;
    $j: 1;
    $translateX: 0;
    $translateY: 0;

    $frameIndex: 0;
    $numberOfFrames: list.length($frames);
    $stepAmount: (100 / $numberOfFrames) * 1%;
    $animation-name: string.unique-id();
}
pixels.scss
The box-shadows will be stored inside the $shadows map

We also need a couple of more variables that are related to the animation. The current frame index, the total number of frames, which we can get simply by getting the length of the $frames map, as well as the percentage amount between the steps and a unique animation name.

Let’s say that we have three frames. This means we want to divide 100% by 3 which results in 33%. We should change the box-shadow every 33%, meaning we should get an animation like the one below:

Copied to clipboard!
@keyframes animation {
    33% { box-shadow: "containing information from first frame"; }
    67% { box-shadow: "containing information from second frame"; }
    100% { box-shadow: "containing information from last frame"; }
}
pixels.scss

Since we have one more depth to our data, we also need one more loop, making the mixin use three different @each rules:

Copied to clipboard!
@each $index, $frame in $frames {
    $frame: map.get($frames, $index);

    @each $list in $frame {
        @each $pixel in $list {
            ...

            @if ($translateX == 0 and $translateY == 0) {
                $translateX: -(list.length($list) * $size) / 2;
                $translateY: -(list.length($frame) * $size) / 2;
             }
        }
    }

    $j: 1;
    $shadows: map.set($shadows, $frameIndex, $shadow);
    $shadow: null;
    $frameIndex: $frameIndex + 1;
}
pixels.scss

The logic can stay the same, the only thing we need to change, is the name of the variables, and the @if rule to also populate the $translateY position alongside with the x. Note that in order to get the proper frame, we need to use map.get in the first loop on $frames and do a loop on the return value.

Lastly, at the end of the three loops, we need to reset $j. (Remember to reset $i in the second loop, this has been omitted from the code example above to keep it short.) Here you can add the newly generated shadow to the $shadows map and reset $shadow back to null. If you @debug the $shadows variable, you will get a map back:

Copied to clipboard!
(
    0: "generated box shadow for the first frame",
    1: "generated box shadow for the second frame",
    2: "generated box shadow for the last frame"
)
pixels.scss

We can use this data in another loop to generate the animation for it, using our unique animation-name:

Copied to clipboard!
@use 'sass:math';

@keyframes #{$animation-name} {
    @for $i from 1 through $numberOfFrames {
        #{math.round($stepAmount * $i)} {
            box-shadow: map.get($shadows, $i - 1);
        }
    }
}
pixels.scss

For each frame, this will get the generated box-shadow and adds it to the animation. The last thing is to also add the animation to the element based on the generated $animaton-name:

Copied to clipboard!
display: block;
width: $size;
height: $size;
box-shadow: map.get($shadows, 0);
transform: translate($translateX, $translateY);

animation-name: $animation-name;
animation-duration: 1000 / $fps + #{ms};
animation-iteration-count: infinite;
animation-timing-function: linear;
pixels.scss

Note that this time, you want to use the first frame as the default box-shadow. And with that, you should get Mario running around:


Summary

And now you know everything about the CSS box-shadow property. If you want to try yourself out in pixel art, there are several online tools out there that you can use, such as pixelartcss.com.

To look into the code in one piece, you can refer to this GitHub repository. If you are also interested in game development, continue your journey with the tutorial below.

Do you already created pixel arts with CSS? Share it with us in the comments below! Thank you for reading through, happy art creating and animating! 🎨

How to Remake Mario in PhaserJS
  • twitter
  • facebook
CSS
Did you find this page helpful?
πŸ“š More Webtips
Frontend Course Dashboard
Master the Art of Frontend
  • check Access 100+ interactive lessons
  • check Unlimited access to hundreds of tutorials
  • check Prepare for technical interviews
Become a Pro

Courses

Recommended

This site uses cookies We use cookies to understand visitors and create a better experience for you. By clicking on "Accept", you accept its use. To find out more, please see our privacy policy.