loading...
Cover image for Creating Pixel Art with CSS

Creating Pixel Art with CSS

jnschrag profile image Jacque Schrag ・6 min read

I have always enjoyed looking at and creating pixel art. Before online pixel makers were a thing, I used to spend hours making my own pixel art in Photoshop with the pencil tool. This article will show you how using CSS (and a tiny bit of HTML), you can use code to make your own pixel art creations.

The Power of box-shadow

While it is 100% possible to create pixel art by creating a bunch of <div>s and changing their background color, that's a lot of <div>s to keep track of and copy if you want to reuse your pixel in multiple places. I prefer to create pixel art with a single <div>, which we can do thanks to the box-shadow property.

box-shadow is commonly used to create a drop shadow effect behind an element, like in the example below.

How does that help us with creating the straight-edged pixel art? By removing the blur & spread parameters from the box-shadow definition, we can straighten out the sides of the shadow.

Next, we want to move the shadow so it is beside the block instead of being behind it. We can do this by adjusting the X- & Y-offset parameters according to the rules below.

X-offset:

  • Positive value moves right
  • Negative value moves left

Y-offset:

  • Positive value moves down
  • Negative value moves up

Shadows inherit their dimensions from the element they're applied to. To move the shadow to the right of the block, we need to set the X-offset to be the same as the width of the block: 20px. If we change the Y-offset to 0, the result looks like if we had two blocks sitting side-by-side.

It's starting to look like pixel art! But this only gives us two "pixels", and we're going to need a lot more than that. Thankfully, the box-shadow property isn't limited to just one effect. By separating our effects with a comma, we can create multiple pixel-looking shadows.

Now that we know how we can use box-shadow, it's time to start making a real piece of pixel art.

Creating a Pixel Cat

We're going to be creating a pixel version of Pusheen. If you're new to making pixel art, I recommend searching for existing art so you have a reference for where your pixels should be placed. I'm going to be recreating this version of pixel Pusheen.

Pusheen pixel art that we will be recreating.

It is made up of 414 pixels (23 columns x 18 rows). To help me easily identify the individual pixels, I've used Photoshop to overlay a grid on the reference image.

Pusheen pixel art with grid overlay to help us easily identify the pixels.

Although you could start drawing your pixel from anywhere, I'm going to start in the uppermost left corner so I don't have to worry about any negative offsets in my box-shadow effects.

I'm also going to use SASS instead of vanilla CSS to avoid writing 414 box-shadow declarations by hand. By utilizing a custom SASS function and lists, we can automate calculating the offset positions and make our code more DRY.

First, I’m going to make some modifications to our #cat block. Instead of applying the box-shadow to the block itself, I’m going to apply it to a pseudo element instead that is absolutely positioned relative to the block. Why? Because box-shadow doesn’t take up space, meaning if I were to put another element next to my cat block, it would sit on top of my shadows. If we make the size of the cat block the final size of our pixel art, we can avoid this problem, but we need the pseudo element to separately define the width/height of our pixels (remember, the size of the shadow is inherited from the element the box-shadow is applied to). This is what those changes look like:

#cat {
  position: relative;
  width: calc(23 * #{$size}); // Pixel size * # of columns
  height: calc(18 * #{$size}); // Pixel size * # of rows
  margin: 1rem;

  &::after {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: $size;
    height: $size;
    // box-shadow will be applied here
  }
}

Next, let’s set up some variables.

// The width/height of each of our "pixels".
$size: 20px;

// Colors
$t: transparent;
$black: #000;
$gray: #cdc9cf;
$dkgray: #a09da1;
$pink: #ffa6ed;

Now we’re going to create a list to track what color each pixel should be. Starting on the left, let’s create a list for the first row.

$first: ($t, $t, $t, $black, $t, $t, $t, $t, $black);

We could create new variables for each of the subsequent rows ($second, $third, etc.), but a better approach is to create a nested list, like so:

$cat: (
  ($t, $t, $t, $black, $t, $t, $t, $t, $black),// 1st Row
  ($t, $t, $black, $gray, $black, $t, $t, $t, $t, $black, $gray, $black)// 2nd Row
  // Additional rows
);

The nested list approach has the benefit of providing us with all the information we need to generate our box-shadow effect for each of the cell: the X/Y positions to calculate our offset and the color of the shadow. We'll access that information with a custom "pixelize" function.

Writing a SASS Function to Draw a Pixel

Our "pixelize" function is going to do the heavy-lifting of turning our list of colors into usable box-shadow definitions. I've provided line-by-line explanations of what this function does below.

@function pixelize($colors, $size) {
  $result: '';
  $numRows: length($colors);

  @for $rowIndex from 1 through $numRows {
    $y: ($rowIndex - 1);
    $row: nth($colors, $rowIndex);
    $numCols: length($row);

    @for $cellIndex from 1 through $numCols {
      $x: ($cellIndex - 1);
      $color: nth($row, $cellIndex);

      $sep: ', ';
      @if $x == 0 and $y == 0 {
        $sep: '';
      }

      $result: $result + '#{$sep}#{$x * $size} #{$y * $size} #{$color}'
    }
  }

  $result: unquote($result);
  @return $result;
}
  • Line 1: The function takes two arguments: the list of $colors and the $size that the pixels should be
  • Line 2: Initializes our $result variable as a string. This is the variable the function will modify and return.
  • Line 3: Returns the number of rows in the list using the built-in length function
  • Line 5: Starts a loop that iterates X times, where X is the number of rows in our list. The $rowIndex will increment by 1 on each loop.
  • Line 6: Calculates the Y-offset of all cells in that row. SASS Lists are index-1 (not index-0), so we subtract 1 from the current index so the 1st row has a Y-offset of 0, 2nd has Y-offset of 1, etc.
  • Lines 7 & 8: Returns the value of the current list item (the list of colors for the row) & calculates its length to determine the number of columns in the row
  • Line 10: Starts a loop to iterate over each column in the row
  • Line 11 & 12: Calculates the X-offset of that cell & returns the corresponding color
  • Lines 14-17: Sets the separator for the box-shadow effects, but removes it for the first cell to ensure a valid property value.
  • Line 19: Updates the $result value to its existing value plus the new cell:
    • Separator
    • X position * $size = X-offset
    • Y position * $size = Y-offset
    • Color
  • Line 23 & 24: $result is a string, so we use the unquote function to remove the containing quotes. Finally, return the result.

The Final Result

Put it all together, and here is our final Pusheen pixel!

Pretty neat! With a little refactoring, the use of CSS Variables, & a smidge of JavaScript, we could even allow users to select their own colors for their cats.

I hope this post has inspired you to make your own pixel art. Even if it hasn't, I hope you've learned how you can use the box-shadow property to create some neat effects in your projects. If you're interested in seeing more pixel art, including examples of how to animate them, check out "Fun Times with CSS Pixel Art" by Geoff Graham on CSS-Tricks.

Discussion

pic
Editor guide
Collapse
ben profile image
Ben Halpern

Wow, I just learned so much about CSS 🤯

Collapse
jnschrag profile image
Jacque Schrag Author

Haha I hope it was useful information! I wondered if I was maybe going too much into the weeds, but ¯_(ツ)_/¯ It made me realize how much of this information I took for granted until it came time to write it down and I realized how many assumptions I was making about what people knew.

Collapse
john_horner1 profile image
john_horner

Nice! I'm not going to do the next-level thing but I'm sure someone is … write a script to create the SASS to create the CSS, a script which can be run on any image!

I did this a while ago, but it's the hard-grinding version of creating table cells with background colours:

johnhorner.info/apple/

I did it with Perl and ImageMagick but I bet there's a Python tool out there.

Collapse
ashleyjsheridan profile image
Ashley Sheridan

I actually did this myself with PHP as the generator: ashleysheridan.co.uk/blog/Single+D...

Modern browsers with enough resources handled it quite well, even at a 1×1 pixel representation.

Collapse
johnhorner profile image
John Horner

Nice! I knew someone smart would have done this!

I’m on my phone right now so I can’t check your source.

  • Does it optimise by re-using colours in a CLUT (colour lookup table)?
  • Does it have a kind of RLE (run length encoding) where a stretch of two or more pixels the same colour are made into a single long element?

Those concepts are used to optimise the file size of GIFs. JPGs I don’t know. They’re more mysterious to me.

Thread Thread
ashleyjsheridan profile image
Ashley Sheridan

It doesn't have any optimization really, I might go back to it and update it, those are some good ideas.

Collapse
jnschrag profile image
Jacque Schrag Author

This is incredible! 27,000 lines of CSS for just the 4 examples on the page is nuts, although I believe it. Very cool!

Thread Thread
ashleyjsheridan profile image
Ashley Sheridan

Yes, I would absolutely not recommend using the technique to the degree I pushed it! My blog post was just about seeing how far I could push it. I did do a full 1×1 example of the Mona Lisa shown there, but it was starting to slow the page down considerably, so I didn't make it part of the post. There is a link to the generator on github should you wish to try it out yourself though!

Collapse
jnschrag profile image
Jacque Schrag Author

Oh gosh 😂 all I can think is the Inception meme “we need to go deeper”. Although that would actually be really cool to be able to recreate any image via box-shadow. Although I wonder if you would hit a limit at how many box-shadow definitions you’re allowed to have before the browser can’t handle it anymore.

Collapse
john_horner1 profile image
john_horner

Only one way to find out!

Thread Thread
y6nh profile image
Hugh

I tried to find that out — though these are conventional blurry shadows, not pixel art: codepen.io/y6nH/pen/YRmVvZ (click with shift or ctrl to add larger numbers).

Thread Thread
jnschrag profile image
Jacque Schrag Author

That's amazing. I stopped after 1500ish, so I didn't find the limit, but that's truly incredible just how much the browser can handle. Although I did start to notice a slow down in how quickly it rendered starting around 800ish.

Collapse
laurieontech profile image
Laurie

This is so cool!

Collapse
jnschrag profile image
Collapse
powerc9000 profile image
Clay Murray

Damn this is cool as hell. Didn't even know pseudo element could have box shadows.

Collapse
jnschrag profile image
Jacque Schrag Author

Yes! Another fun fact for you: the box-shadow property can be animated. Which means you can make moving pixels like this: codepen.io/jschrag/pen/PrYrQE

But if you apply the box-shadow to a pseudo element & animate it, it actually becomes more performant in the browser. Here’s an article about that: alligator.io/css/transition-box-sh...

Collapse
anpos231 profile image
anpos231

From what I understand, this article is about creating an illusion of transforming box-shadow by changing it's opacity.
The problem with animating box-shadow is that it triggers repaints on every change.

Thread Thread
jnschrag profile image
Jacque Schrag Author

Good catch! Looks like I wasn't paying close enough attention. That said, if your animation has two states, one way to approach it would be to have two pseudo elements (::before for default state & ::after for 2nd state) with box-shadow applied and alternate the opacity on those.

Or just make a gif. :)

Thread Thread
gypsydave5 profile image
David Wickes

Or just make a gif. :)

🤣🤣🤣

Collapse
demaine profile image
Colin Demaine

The box-shadow technique is definitely a fun way to make pixel art with CSS. I like your approach in explaining the technique to make it simple and accessible. In comparison, my first attempt of using this technique was a bit extreme, to say the least: codepen.io/demaine/full/rRvdJZ

Collapse
jnschrag profile image
Jacque Schrag Author

I remember seeing this when you first created it! It’s so cool!! In another comment we were talking about the browser limits of using box-shadow for this kind of thing, so it’s great to see your piece and read how you addressed that issue.

Collapse
rolandcsibrei profile image
Roland Csibrei

Cool approach even though it is incredibly insane to create pixel art with this technique. We need "crazy" developers like you to push the limits further and further. Thanks for sharing! Have a great day!

Collapse
johnhorner profile image
John Horner

I've been quietly obsessing over this topic ever since I read your article and I have written a Perl module to turn images into data structures.

This video shows an image being uploaded and rendered by two different methods, one with elements 1×1 pixels in size, the other using your shadow method. They load very differently, as you can see.

The shadow method is quite a bit smaller in file size, though of course both are laughably huge compared to the original GIF itself.

I optimised a bit by using the most common colour as the background, so no "pixels" need to be rendered for that colour, and by creating classes for each colour with names as short as possible.

I'm going to do RLE (run-length encoding) soon but it's making my head hurt trying to figure out how to do it.

I used a GIF because the maximum colours is 256 as opposed to 16 million for JPG. The image in question is 128×128.

Collapse
jnschrag profile image
Jacque Schrag Author

This is really cool! Until I posted and had some discussion in the comments, I'd never considered creating a tool to generate those different pixels programmatically. Optimizing by coloring the background is quite clever too!

Collapse
willsmart profile image
willsmart

Such a cool technique! Great post.

I noticed you're drawing a lot of transparent shadow pixels though, they're easily gotten rid of via something like:

Which might speed up rendering a bit (depending on the how many transparent pixels there are and if there are mysterious render optimisations in play).

Collapse
jnschrag profile image
Jacque Schrag Author

Yes, drawing the transparent box-shadows aren't necessary, so that's a great improvement! Thanks for sharing. :D

Collapse
tailcall profile image
Anton Istomin

There must be a webpack loader for that!

Collapse
jnschrag profile image
Jacque Schrag Author

Haha I mean...there’s a webpack loader for everything else. 😂

Collapse
mobidi profile image
Mobidi

Really nice exercice ! Thanks, I'll try to make some :D

Collapse
jnschrag profile image
Jacque Schrag Author

Thanks! Would love to see what you come up with! :)

Collapse
mobidi profile image
Mobidi

Oh and just changing the shape of your orignal pixel with a border radius seems to make some really cool stuff happens.

Collapse
worksnakes profile image
Christopher Andert

I never thought to use box shadow multiple times on the same element. That's a nifty trick!

Collapse
jnschrag profile image
Jacque Schrag Author

Yeah! It's one way that you can create truly incredible CSS art like this codepen.io/ivorjetski/pen/xMJoYO

Collapse
jaymeedwards profile image
Jayme Edwards 🍃💻

Super cute article (and artwork). Nice!

Collapse
jnschrag profile image
Jacque Schrag Author

Thanks! Can’t claim credit for the original artwork though, I found it online :)

Collapse
jaymeedwards profile image
Jayme Edwards 🍃💻

Oops, guess I missed that. 🤦🏻‍♂️

Collapse
andreujuanc profile image
Juan C. Andreu

Is this pusheen but kitten? D:

Collapse
jnschrag profile image
Jacque Schrag Author

Kitten because it's small? If so, then yes!

Collapse
andreujuanc profile image
Collapse
harrymckillen profile image
Harry McKillen

I've been doing this for a little while, but without that great little pixelize function. Which I can now "borrow"!

Collapse
jnschrag profile image
Jacque Schrag Author

Please do! Nobody should be writing that many box-shadow effects by hand 😂

Collapse
harrymckillen profile image
Harry McKillen

In lieu of sitting down to figure out how to break the problem apart, like a luddite, I do it! :) You've saved me a lot of time.

Collapse
nothinbutblood profile image
Sriram

Great work I'm sriram fullstack javascript developer I have a whatsapp group dedicated to coders so that you can chat and collaborate on fun hobby projects with real people if you are interested please ping me at +918970787208

Collapse
webdeasy profile image
WebDEasy

Very nice! Good work 😇

Collapse
jnschrag profile image
Jacque Schrag Author

Thanks so much!

Collapse
jsvcycling profile image
Josh Vega

My mind literally just exploded with how awesome this is! Thanks for sharing!!

Collapse
jnschrag profile image
Jacque Schrag Author

Thanks so much!

Collapse
k_penguin_sato profile image
K-Sato

sick!!

Collapse
jnschrag profile image
Collapse
daveskull81 profile image
dAVE Inden

This is awesome. A great example of the power of CSS and a preprocessor like SASS. Plus, pixel art is super cute and fun and makes for a great article topic in my opinion. Thanks for this article!

Collapse
jnschrag profile image
Jacque Schrag Author

Thank you!! Glad you enjoyed it. :)

Collapse
ananyaneogi profile image
Ananya Neogi

This is so cool! 😀
I've always loved playing with pixel art and have spent way too much time at Make 8-bit Art

Collapse
jnschrag profile image
Jacque Schrag Author

Thanks! There’s so many online tools for this it’s great. Although for whatever reason, I find using them more tedious, even though writing out the colors for all those cells wasn’t exactly quick lol

Collapse
washingtonsteven profile image
Steven Washington

This is amazing and adorable. Love it!

Collapse
jnschrag profile image
Collapse
unlimiter profile image
Unlimiter

Pretty smart. I like the idea!

Collapse
aeiche profile image
Aaron Eiche

What a brilliant and creative use for box-shadow. This is absolutely delightful!

Collapse
alebiagini profile image
aleBiagini

Very Nice tut! Congratss

Collapse
jnschrag profile image
Collapse
sarneeh profile image
Jakub Sarnowski

This is amazing! :D Awesome work!

Collapse
rixcy profile image
Rick Booth

So so good, I'll definitely be playing around with this when I get chance!

Collapse
jnschrag profile image
Jacque Schrag Author

Thanks! Would love to see any pixels you end up creating!

Collapse
macskeptic profile image
♥ ☕

that escalated quickly... here's a pixel, 2 pixels, 3 pixels, cat!

good stuff though. makes me want to go play with it : )

Collapse
anpos231 profile image
Collapse
alliwalk profile image
Allison Walker

Very impressive!

BTW, the link to your github page doesn't seem to be working.

Collapse
jnschrag profile image
Jacque Schrag Author

Thanks!

Do you mean on my profile? If so, it's working fine for me. But if you're interested, it's github.com/jnschrag

Collapse
alliwalk profile image
Allison Walker

You're welcome.

Yes it goes to jacqueschrag.com/. I get a 404. Just FYI.

Thread Thread
jnschrag profile image
Jacque Schrag Author

Oh, you mean my personal site. Yeah, that's currently offline. :) Thanks for letting me know!

Collapse
asfo profile image
Asfo

I did this when I was bored just to create something like this but in a simpler way :P

codepen.io/asfo/pen/QxWzVd