Box-shadow Pixel Images

This really isn't practical. It's just to show you can do it, and for fun. You're better off with an icon font or regular images instead.

Box-shadow. A property intended to make elements have nice drop or inset shadows. However that's not all people use it for and indeed some usages can be quite esoteric. Today we're going to see how we can turn images into box-shadows rather like pixel art.

Large Blue Mario Mushroom The end result will look something like this. Image source.

Getting the Data

First we need an image. For simplicity, let's use Blue Mario Mushroom. This is the same image as above except each square only takes up one pixel. You can use any image you like, but small ones are easier and will have smaller output. Output doesn't necessarily scale linearly with the image dimensions as we can skip completely transparent pixels.

Next we need to read out the colors of each pixel in the image. I'm going to use canvas and JavaScript to do so as most people reading this article likely already know JavaScript.

<img id="image" href="deathcap.png">
<script>
    window.onload = function ()
    {
        var canvas, colors, x, y, color, r, g, b, a, output = "";

        // Create a canvas and draw the image to it.
        canvas = document.createElement("canvas").getContext("2d");
        canvas.drawImage(image, 0, 0);

        // Get the image data from the canvas.
        colors = canvas.getImageData(0, 0, image.width, image.height);

        // Use the data width and height instead of the image's as the backing data may not be 1-1.
        for (y = 0; y < colors.height; y++)
        {
            for (x = 0; x < colors.width; x++)
            {
                // Get the current pixel's color components. There are four components (RGBA) to a
                // color so we need to account for that in our offsets.
                r = colors.data[(y * colors.width + x) * 4];
                g = colors.data[(y * colors.width + x) * 4 + 1];
                b = colors.data[(y * colors.width + x) * 4 + 2];
                a = colors.data[(y * colors.width + x) * 4 + 3];

                // If the color is completely transparent we can skip it.
                if (a)
                {
                    // Writes our offset into the image data. Also sets the box-shadow's blur and
                    // spread radius to zero so we get hard edges.
                    output += x + "em " + y + "em 0 0 ";

                    // Transparent pixels should be written out with the rgba(...) syntax.
                    if (a < 255)
                    {
                        output += "rgba(" + r + "," + g + "," + b + "," +
                                (a / 255).toFixed(2).replace(/^0|0$/,"") + ")";
                    }
                    else
                    {
                        // Values 9 or less need to be padded with a leading zero otherwise the
                        // color output will be too short and invalid.
                        r = (r < 10 ? "0" : "") + r.toString(16);
                        g = (g < 10 ? "0" : "") + g.toString(16);
                        b = (b < 10 ? "0" : "") + b.toString(16);

                        output += "#" + r + g + b;
                    }

                    output += ",";
                }
            }
        }

        console.log(output.substring(0, output.length - 1));
    }
</script>

The above will generate a box-shadow for each non-transparent pixel, and the output will look like the following long and ugly line of code.

The above can then be set as the box-shadow value for a given selector, like so:

.thing {
    box-shadow: 5em 0em 0 0 #ffffff,6em 0em 0 0 #ffffff,...; /* Truncated for brevity. */
}

Further Styles

The box-shadow alone is not enough. A box-shadow is only as large as the element to which it is applied. Therefore at minimum we also need to set a width and height. As you'll notice, the box-shadow offsets generated by the above code uses em units. This is intentional as we can then define the width and height of the element in em and use font-size to set our "pixel" size. In addition, any text inside such an element would be pretty much useless so we can remove it by setting the color to transparent and hiding the overflow.

.thing {
    display: inline-block;
    width: 1em;
    height: 1em;
    overflow: hidden;
    font-size: 6px;
    color: transparent;
}

However this will still allow it to overlap with other elements because box-shadow does not reserve any space in the layout. To change that we need to set a margin on it. This is easy to do by setting the right and bottom margins to be equal to the height and width of the image. This may introduce a very small gap in Firefox between the box-shadows which I have found can be solved with a simple trick, also shown below.

.thing {
    transform: translate(0,0);
    margin-right: 16em;  /* Depends on input image size. */
    margin-bottom: 15em; /* Depends on input image size. */
}

The script from above can be modified to output all this information in one go if you're interested in having a complete self-contained generator.

Animation

This effect can definitely be animated. The process is the same as any other animation, just create an @keyframes block which replaces the long box-shadow declaration with a new one as many times as you want. If the number of "pixels" and order of the "pixels" (their offsets) is the same, the browser may even tween them for you providing a cross-fade between the old and new colors.

And so you have it. For a complete all-in-one example of this, please see the demo page. Thank you for reading.

Credits: I can't claim to have come up with the original idea myself. I first saw it quite a while back at The Shapes of CSS, curated by Chris Coyier. The shape in particular that lead me to try making my own by hand (resulting in a calendar icon) was the Space Invader by Vlad Zinculescu. Finally, I came up with my approach to generating these before I found image2css by Alexander Hripak, but I feel his site is also worth a mention. Definitely check all of these out for more fun CSS stuff.