Advice for Complex CSS Illustrations

Avatar of Jhey Tompkins
Jhey Tompkins on (Updated on )

DigitalOcean provides cloud products for every stage of your journey. Get started with $200 in free credit!

If you were to ask me what question I hear most about front-end development, I’d say it’s“How do I get better at CSS?”. That question usually comes up after sharing a CSS illustration I have made. It’s something I love to do over on CodePen.

To many, CSS is this mythical beast that can’t be tamed. This tweet from Chris made me chuckle because, although ironic, there’s a lot of truth to it. That said, what if I told you that you were only a few properties and techniques away from creating anything you wanted? The truth is that you are indeed that close.

I’ve been wanting to compose an article like this for some time, but it’s a hard topic to cover because there are so many possibilities and so many techniques that there’s often more than one way to accomplish the same thing. The same is true with CSS illustrations. There’s no right or wrong way to do it. We’re all using the same canvas. There are simply so many different tools to get those pixels on the page.

While there is no “one size fits all” approach to CSS illustration, what I can offer is a set of techniques that might help you on your journey.

Time and practice

CSS illustration takes lots of time and practice. The more accurate you want to be and the more complicated the illustration, the longer it’s going to take. The time-consuming part isn’t usually deciding on which properties to use and how, but the tinkering of getting things to look right. Be prepared to get very familiar with the styles inspector in your browser dev tools! I also recommend trying out VisBug if you haven’t.

Two fantastic CSS artists are Ben Evans and Diana Smith. Both have recently talked about time consumption when referring to CSS illustration.

Screenshot of a realistic looking woman gazing up with her hards across her chest.
Diana’s PureCSS Gaze took her two long weekends to complete. She talks about some of her techniques here and here. “If you have the time, patience, and drive, it is certainly possible,” she says.

I posted a meme-like picture about a cup and Ben’s response summed things up perfectly:

I was tempted to create this in CSS when I first saw the tweet but then thought my reply would take about a month.

It takes time!


Tracing is perfectly acceptable

We often have an idea of what it is that we want to illustrate. This article isn’t about design, after all.; it’s about taking an image and rendering it with the DOM and CSS. I’m pretty sure this technique has been around since the dawn of time. But, it’s one I’ve been sharing the last few months.

  • Find or create an image of what it is you want to illustrate.
  • Pull it into your HTML with an <img> tag.
  • Position it in a way that it will sit underneath your illustration.
  • Reduce the image opacity so that it’s still visible but not too overpowering.
  • Trace it with the DOM.

To my surprise, this technique isn’t common knowledge. But it’s invaluable for creating accurate CSS illustrations.

See this trick in action here:

And try it out here:

Pay attention to responsiveness

If there are two takeaway techniques to take from this article, let it be the “Tracing” one above and this next one. 

There are some fantastic examples of CSS illustration out there. But the one unfortunate thing about some of them is that they aren’t styled — or even viewable — on small screens. We live in an age where first impressions with tech are important. Consider the example of a keyboard illustrated with CSS. Someone comes across your work, opens it up on their smartphone, and they’re greeted with only half the illustration or a small section of it. They probably missed the coolest parts of the demo!

Here’s my trick: leverage viewport units for your illustrations and create your own scaled unit. 

For sizing and positioning, you either have the option of using a scaled unit or percentage. This is particularly useful when you need to use a box shadow because the property accepts viewport units but not percentages.

Consider the CSS logo I created above. I found the image I wanted to use and popped it in the DOM with an img tag.

<image src='egghead.png'/>
img {
  height: 50vmin;
  left: 50%;
  opacity: 0.25;
  position: fixed;
  top: 50%;
  transform: translate(-50%, -50%);

The height, 50vmin, is the desired size of the CSS illustration. The reduced opacity allows us to “trace” the illustration clearly as we progress.

Then, we create our scaled unit.

  * image dimensions are 742 x 769
  * width is 742
  * height is 769
  * my desired size is 50vmin
:root {
  --size: 50;
  --unit: calc((var(--size) / 769) * 1vmin);

With the image dimensions in place, we can create a uniform unit that’s going to scale with our image. We know the height is the largest unit, so we use that as a base to create a fractional unit.

We get something like this:

--unit: 0.06501950585vmin;

That looks awkward but, trust me, it’s fine. We can use this to size our illustration’s container using calc().

.egg {
  height: calc(769 * var(--unit));
  position: relative;
  width: calc(742 * var(--unit));
  z-index: 2;

If we use either percentages or our new --unit custom property to style elements within the container of our CSS illustration, we will get responsive CSS illustrations… and all it took was a few lines of math using CSS variables!

Resize this demo and you’ll see that everything stay in proportion always using 50vmin as the sizing constraint.

Measure twice, cut once

Another tip is to measure things. Heck, you van even grab a tape measure if you’re working with a physical object!

This may look a little funky but I measured this scene. It’s the TV combo unit I have in my lounge. Those measurements equate to centimeters. I used those to get a responsive unit based on the actual height of the TV. We can give that number — and all others — a name that makes it easy to remember what it’s for, thanks to custom properties.

:root {
  --light-switch: 15;
  --light-switch-border: 10;
  --light-switch-top: 15;
  --light-switch-bottom: 25;
  --tv-bezel: 15;
  --tv-unit-bezel: 4;
  --desired-height: 25vmin;
  --one-cm: calc(var(--desired-height) / var(--tv-height));
  --tv-width: 158.1;
  --tv-height: 89.4;
  --unit-height: 42;
  --unit-width: 180;
  --unit-top: 78.7;
  --tv-bottom: 114.3;
  --scaled-tv-width: calc(var(--tv-width) * var(--one-cm));
  --scaled-tv-height: calc(var(--tv-height) * var(--one-cm));
  --scaled-unit-width: calc(var(--unit-width) * var(--one-cm));
  --scaled-unit-height: calc(var(--unit-height) * var(--one-cm));

As soon as we calculate a variable, we can use it everywhere. I know my TV is 158.1cm wide and 89.4cm tall. I checked the manual. But in my CSS illustration, it will always scale to 25vmin.

Use absolute positioning on all the things

This one will save you a few keystrokes. More often than not, you’ll be looking to absolutely position elements. Save yourself and put this rule somewhere.

/* Your class name may vary */
.css-illustration *,
.css-illustration *:after,
.css-illustration *:before,
.css-illustration:before {
  box-sizing: border-box;
  position: absolute;

Your keyboard will thank you!

Positioning is a tricky concept in CSS. You can read up on it in the CSS Almanac for more information on how to use it.

Or, have a play with this little positioning playground:

Stick to an approach

This is by far the hardest thing to do. How do you approach a CSS illustration? Where do you even start? Should you start with the outermost part and work your way in? That doesn’t work so well.

Odds are that you’ll try some approaches and find a better way to go about it. You’ll certainly do a little back-and-forth but, the more you practice, the better you’ll get at spotting patterns and developing an approach that works best for you.

I tend to relate my approach to how you’d go about creating a vector image where illustrations are made up of layers. Split it up and sketch it on paper if you need to. But, start from the bottom and work your way up. This tends to mean larger shapes first, and finer details later. You can always tinker with the stacking index when you need to move elements around.

Maintain a solid structure for your styles

That leads us to the structure. Try to avoid a flat DOM structure for your illustration. Keeping things atomic makes it easier to move parts of your illustration. It will also makes it much easier to show and hide parts of the illustration or even animate them later. Consider the CSS Snorlax demo. The arms, feet, head, etc. are separate elements. That made animating the arm a lot easier than if I had tried to keep things together since I could simply apply the animation to the .snorlax__arm-left class.

Here’s a timelapse shot of me creating the demo:

Handling awkward shapes

There’s a pretty good article right here on CSS-Tricks for creating shapes with CSS. But what about more “awkward” shapes, like a long curve or even an outer curve? In these scenarios, we need to think outside the box. Properties such as overflow, border-radius, and clip-path are big helpers.

Consider this CSS Jigglypuff demo. Toggle the checkbox.

That’s the key for creating curved shapes! We have an element much larger than the body with a border-radius applied. We then apply overflow: hidden to the body to cut that part off.

How might we create an outer curve? This one’s a little tricky. But a trick I like to use is a transparent element with a thick border. Then apply a border-radius and clip the excess, if required.

If you hit the toggle, it reveals the element we are using to go across that corner. Another trick might be to overlay a circle that matches the background color. This is fine until we need to change the background color. It’s OK if you have a variable or something in place for that color. But, it could make things a little harder to maintain.

clip-path is your friend

You might have noticed a couple of interesting CSS properties in that last demo, including clip-path. You’ll most likely need clip-path if you want to create complex CSS shapes. It’s especially handy for cutting off bits of elements when hiding parent box overflow doesn’t do.

Here’s a little demo I built some time ago that showcases different clip-path possibilities.

There’s also this demo that takes ideas from the “Shapes of CSS” article and re-created with clip-path.

border-radius is your other friend

You’re going to need border-radius to create curves. One uncommon trick is to use a “double” syntax. This allows you to create a horizontal and vertical radius for each corner.

Play with this demo to really appreciate the power of border-radius. I advocate using percentages across the board in order keep things responsive.

Shading techniques

You’ve got all the shapes, everything is nicely laid out, and all the right colors are in place… but something still looks off. Odds are that it’s a lack of shading.

Shading adds depth and create a realistic feel. Consider this ecreation of a Gal Shir illustration. Gal is fantastic at using shades and gradients to make beautiful illustrations. I thought it would be fun to do a recreate it and include a switch that toggles the shading to see just what a difference it makes.

Shading effects are often created with a box-shadow and background-image combination.

The key thing with these properties is that we can stack them in a comma-separated list. For example, the cauldron in the demo has a list of gradients that are being used across the body.

.cauldron {
    radial-gradient(25% 25% at 25% 55%, var(--rim-color), transparent),
    radial-gradient(100% 100% at -2% 50%, transparent, transparent 92%, var(--cauldron-color)),
    radial-gradient(100% 100% at -5% 50%, transparent, transparent 80%, var(--darkness)),
    linear-gradient(310deg, var(--inner-rim-color) 25%, transparent), var(--cauldron-color);

Note that radial-gradient() and a linear-gradient() are being used here and not always with perfectly round numeric values. Again, those numbers are just fine. In fact, you’ll spend a lot of time tweaking and tinkering with things in the style inspector.

It’s generally the same working with box-shadow. However, with that, we can also use the inset value to create tricky borders and additional depth.

.cauldron__opening {
    0 0px calc(var(--size) * 0.05px) calc(var(--size) * 0.005px) var(--rim-color) inset,
    0 calc(var(--size) * 0.025px) 0 calc(var(--size) * 0.025px) var(--inner-rim-color) inset,
    0 10px 20px 0px var(--darkness), 0 10px 20px -10px var(--inner-rim-color);

There are certainly times where it will make more sense to go with filter: drop-shadow() instead to get the effect you want.

Lynn Fisher’s is a brilliant example of these properties in action. Have a poke around on that site and inspect some of the illustrations for great ways to use box-shadow and background-image in illustrations.

box-shadow is so powerful that you could create your entire illustration with it. I once joked about creating a CSS illustration of a dollar.

I used a generator to create the illustration with a single div. But Alvaro Montoro took it a little further and wrote a generator that does it with box-shadow instead.

Preprocessors are super helpful

While they aren’t required, using preprocessors can help keep your code neat and tidy. For example, Pug makes writing HTML faster, especially when it comes to using loops for dealing with a bunch of repeating elements. From there, we can scope CSS custom properties in a way that we only need to define styles once, then overwrite them where needed.

Here’s another example that demonstrates a DRY structure. The flowers are constructed with the same markup, but each has its own index class that is used to apply scoped CSS properties.

The first flower has these properties:

.flower--1 {
  --hue: 190;
  --x: 0;
  --y: 0;
  --size: 125;
  --r: 0;

It’s the first one, so all the others are based off it. Notice how the second flower is off to the right and up slightly. All that takes is assigning different values to the same custom properties:

.flower--2 {
  --hue: 320;
  --x: 140;
  --y: -75;
  --size: 75;
  --r: 40;

That’s it!

Go forth, use these tips, come up with your own, share them, and share your CSS masterpieces! And hey, if you have your own advice, please share that too! This is definitely the sort of thing that is learned through lots of trial and error — what works for me may look different from what works for you and we can learn from those different approaches