DEV Community

Cover image for How to create pure CSS illustrations and animate them - Part 1
Agathe Cocco
Agathe Cocco

Posted on

How to create pure CSS illustrations and animate them - Part 1

I've always had a great interest in design, illustrations and colour palettes, but having spent the last few years focusing on becoming a better front-end developer, I've been left with little time to practice my creative skills. When I was first introduced to CSS images, I couldn't wait to give it a try. At last I could play around with shapes and colors, and explore and develop my creativity while coding!

But first, what is a pure CSS image?

A pure CSS image is an illustration that has been built with HTML and CSS. It excludes the use of image file imports, or code generated by exporting graphics in illustration software. In other words, it's an image that has been entirely manually coded in a code editor, using HTML and CSS only.

The principle is simple: with HTML, you can create as many divs as you like, which will each represent the basic shape of a component of the final image. With CSS properties such as gradients, transform, border-radius, shadows and so on, you can transform these basic shapes and arrange them into a nice illustration.

Why CSS images?

But what’s even the point of CSS images, you ask? CSS is primarily meant to style web pages after all, and there are more adapted and efficient tools to produce quality and lightweight graphics for web design (think SVG). Indeed, not only does using CSS for illustration have some design limitations, the result can be overly complex, time consuming and face some major cross browser/device issues.

So there is definitely a place and time for CSS images. However, creating CSS Illustrations is a great way to get better at CSS and to be introduced to new tools and concepts, such as animations, preprocessors or more obscure CSS properties. I feel my CSS skills have skyrocketed ever since I started coding CSS images, and I have become much more familiar with concepts I didn't know much about, like the wide variety of CSS selectors, 3D transforms and keyframes animations.

Who is it for?

CSS illustrations are great for:

  • Illustrators or designers hoping to use their designing skills to learn, or get more confident with html/css
  • Front-end developers hoping to work on their creativity and develop a good eye for design
  • Anyone wanting to have a bit of fun while strengthening their CSS skills
  • Anyone hoping to connect with a community, inspire and be inspired
  • Or anyone up for a good challenge

In this series, we'll learn how to create three CSS illustrations, ranging from simple to complex. We'll learn the basics of CSS animations and how to use them to animate our illustrations. Along the way, we'll find out what concepts, tools and techniques can help speed up our workflow.

Part 1: Learning basics and workflow tips with a CSS Smiley Face
Part 2: Intro to CSS animations with a CSS Polaroid
Part 3: More advanced techniques with a CSS Lighthouse Scene

While this tutorial series doesn't require any advanced knowledge of web development, I'm assuming you are familiar at least familiar with HTML and CSS.

A couple of suggestions before we begin:

  • Open a CodePen account, if you don't have one already. It'll take away the pain of having to set up a project, especially since we'll be using CSS preprocessors and templating languages.
  • Use either Chrome or Firefox, as other browsers have proven to be buggy with CSS illustrations.

Basics

Okay enough talk, let’s get started with our first CSS image!

Here’s a circle:

<div class="circle"></div>
Enter fullscreen mode Exit fullscreen mode
html,
body {
  height: 100%;
  width: 100%;
  overflow: hidden;
  padding: 0;
  margin:0;
}
body {
  background: #FEEE9D;
}
.circle {
  position: absolute;
  width: 300px;
  height: 300px;
  transform: translate(-50%, -50%);
  top: 50%;
  left: 50%;
  border-radius: 50%;
  background-color: #FBD671;
}
Enter fullscreen mode Exit fullscreen mode

Nothing impressive so far.

We're going to add a few elements to turn this circle into a smiley face. The circle will be the head, and we can add eyes and a mouth:

<div class="head">
  <div class="face">
    <div class="mouth"></div>
    <div class="eye-group">
      <div class="eye eye-left"></div>
      <div class="eye eye-right"></div>
    </div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode
html,
body {
  height: 100%;
  width: 100%;
  overflow: hidden;
  padding: 0;
  margin: 0;
}
body {
  background: #FEEE9D;
}
* {
  position: absolute;
}
.head {
  width: 300px;
  height: 300px;
  transform: translate(-50%, -50%);
  top: 50%;
  left: 50%;
  border-radius: 50%;
  background-color: #FBD671;
}
.face {
  width: 150px;
  height: 170px;
  left: 75px;
  top: 75px;
}
.mouth {
  width: 100%;
  height: 75px;
  bottom: 0;
  left: 0;
  background-color: #20184E;
  border-radius: 10px 10px 150px 150px;
  border: 5px solid #20184E;
}
.eye-group {
  width: 150px;
  height: 50px;
  top: 10px;
  left: 0;
}
.eye {
  background-color: #20184E;
  width: 30px;
  height: 50px;
  border-radius: 50%;
}
.eye-left {
  left: 15px;
}
.eye-right {
  right: 15px;
}
Enter fullscreen mode Exit fullscreen mode

Okay, let’s have a look at what’s going on here.

Main container

We have a main container, in this case the .head element, that contains every other HTML elements. This is, as you would expect, to center our illustration, and serve as a reference for the positioning of everything else.

Absolute positioning

All elements have the position: absolute property. This allows, with the help of the top/right/bottom/left properties, to precisely position them relatively to their parent. It's important to note that absolute positioning removes elements from the natural flow of the page. It means that the position of an element with that property will not affect the position of other elements, and will not be affected by their position either. Assigning the position: absolute property to all elements by default will ensure they will all be independent from each other. More on positioning here.

* {
  position:absolute;
}
Enter fullscreen mode Exit fullscreen mode

Nesting

Following this logic, we use nesting to group elements together. Observe the .face element. It is not an actual component of the image, but used only to group the eyes and the mouth together. Both .mouth and .eye-group 's position (and size, if we use % instead of px) will refer to .face. If, for example, we decide we want to reposition the face on the head later, we can do it at once instead of having to tweak the properties of both .mouth and .eyes. This is basic CSS logic and won't be new to you if you have experience in UI development. As a rule of thumb, nesting allows a clearer structure because it groups elements that belong together, and make the illustration easier to manipulate.

Stacking

Using absolute positioning allow us to have more control on the stacking order of the elements. Naturally, the stacking order follows the flow of the HTML elements. The first object will be at the bottom of the pile (at the far back), while the last one will be on top (closest to you). With the z-index property, we can change the position of each element in the pile and make them overlap how we want to, provided these elements belong to the same stacking context. The higher the value of z-index, the higher it'll be in the pile.

Stacking contexts are a bit tricky and don't always behave as you would expect them to. You can read more about what triggers the creation of a new stacking context here.

Classes only

We only use CSS classes to target and style our HTML elements as we don't want to be bothered by specificity issues.


Phew, that was a lot already. But these are very important concepts of CSS illustrations, and CSS in general. Once you are comfortable with positioning and stacking, your CSS skills will improve drastically.

Now that we have a basic understanding of how to position elements, let’s jazz up our smiley face a little. I want to add a few details like a tongue, pupils and a shadow.

The shadow is straightforward but adds a nice touch:

.head {
  width: 100%;
  height: 100%;
  background-color: #FBD671;
  border-radius: 50%;
  box-shadow:inset -10px -10px 0px #EFBB42;
}
Enter fullscreen mode Exit fullscreen mode

The tongue is a simple pink circle, but we only want to show part of it to create the illusion that it's inside the mouth. To do this, we nest .tongue in .mouth and apply the overflow: hidden property to .mouth.

<div class="head">
  <div class="face">
    <div class="mouth">
      <div class="tongue"></div>
    </div>
    <div class="eye-group">
      <div class="eye eye-left"></div>
      <div class="eye eye-right"></div>
    </div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode
.mouth {
  width: 100%;
  height: 75px;
  background-color: #20184E;
  left: 0;
  bottom: 0;
  border-radius: 10px 10px 150px 150px;
  border: 5px solid #20184E;
  overflow:hidden;
}
.tongue {
  width: 100px;
  height: 80px;
  left: 25px;
  top: 30px;
  background-color: #F15962;
  border-radius: 50%;
}
Enter fullscreen mode Exit fullscreen mode

And we can add pupils with a simple oval inside our .eye elements.

<div class="container">
  <div class="head">
    <div class="face">
      <div class="mouth">
        <div class="tongue"></div>
      </div>
      <div class="eye-group">
        <div class="eye eye-left">
          <div class="pupil"></div>
        </div>
        <div class="eye eye-right">
          <div class="pupil"></div>
        </div>
      </div>
    </div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode
.pupil {
  width: 10px;
  height: 15px;
  top: 5px;
  background-color: #FBD671;
  border-radius: 50%;
}
Enter fullscreen mode Exit fullscreen mode

It's looking much better already.

More advanced concepts

Now that we have built our first CSS image, let’s take a step back and look at how we can improve our code. While these steps aren't necessary at all to build CSS images, I find they greatly improve my workflow, and helped me getting much better at CSS in general.

CSS Preprocessors

If you are familiar with CSS preprocessors you know there’s a better way to do this. I won’t delve into how to set up a preprocessor as this is beyond the scope of this article. Instead I strongly recommend you use Codepen, which has all preprocessors built in. You just have to select the one you want in the CSS settings and you’re all set.

I personally like to use SASS/SCSS, but feel free to use any of the options available.

The best known feature of preprocessors is the ability to use variables. Preprocessor variables work just like javascript ones. You declare them and assign them a value once, then you can use and reuse them throughout your code. This is a great tool as it avoids repetition and gives you a lot more flexibility. Let's implement a few variables in our CSS. A good use for variables is to define colors.

A SCSS variable always starts with the $ sign:

$black: #20184E;
Enter fullscreen mode Exit fullscreen mode

You can name it whatever you like, but it must start with $ and be assigned a valid CSS property.

Then you can use it like so:

background-color: $black;
Enter fullscreen mode Exit fullscreen mode

You can see where this would come handy. If we later decide that we want to change this particular color, we only have to do it in one place and it will be reflected everywhere the variable is used in our code. Also when it comes to colors, it's a lot easier to remember the name of a variable you created than a RGB or HEX value.

I've added my new color variables to the top of the code so we can find them easily. Declaring variables in the global scope (as opposed to declaring them in a selector) will make sure we can use them everywhere.

$black: #20184E;
$head-color: #FBD671;
$background: #FEEE9D;
$tongue-color: #F15962;
Enter fullscreen mode Exit fullscreen mode

Now we can change our css properties so they point to the variables instead of the hard coded HEX values.

Eg:

.mouth {
  width: 100%;
  height: 75px;
  bottom: 0;
  background-color: $black;
  border: 5px solid $black;
  border-radius: 10px 10px 150px 150px;
  overflow: hidden;
}
Enter fullscreen mode Exit fullscreen mode

CSS now has native variables that pretty much make the preprocessor ones unnecessary. However, preprocessors offer a lot more than variables and are still extremely useful in many other ways.

SCSS also comes with many helper functions, including color functions. These functions are quite powerful as they allow you to manipulate colors very easily.

Let's have a look at the inset shadow applied to the head. Right now we are using a hard coded HEX value, which I've had to manually look up. We are going to use the darken function to generate this value instead:

.head {
  border-radius: 50%;
  width: 100%;
  height: 100%;
  background-color: $head-color;
  box-shadow: inset -10px -10px 0px darken($head-color, 20%);
}
Enter fullscreen mode Exit fullscreen mode

Basically, the function takes $head-color as a parameter then and increases the amount of darkness by 20%.

I love using color functions because not only do they eliminate the need to resort to some design software to generate colors, they also help with creating a more harmonious color palette for your illustration.

Another great feature of SASS is nesting. Nesting allows you to nest selectors within parent selectors in order to create shortcuts. It creates a better hierarchy and readability of your CSS, and avoids repeating selectors over and over.

For example

.pink {
  background-color: pink;
  .purple {
    background-color: purple;
  }

}
Enter fullscreen mode Exit fullscreen mode

will be compiled to:

.pink { 
  background-color: pink;
}
.pink .purple { 
  background-color: purple;
} 
Enter fullscreen mode Exit fullscreen mode

SASS also offers the & selector, which basically refers to the parent selector. So

.pink {
  background-color: pink;
  &.purple{
    background-color: purple;
  }
}
Enter fullscreen mode Exit fullscreen mode

will be compiled to:

.pink { 
  background-color: pink;
}
.pink.purple { 
  background-color: purple;
} 
Enter fullscreen mode Exit fullscreen mode

Using the & selector will not target child elements that have a class .purple like in the example above. It will target the parent element that has the classes .pink AND .purple.

Beware of too much nesting as it can make your code overly complex and less readable, which would be the opposite of what we're aiming for. As a rule, I tend to go for a maximum of 3 levels of nesting.

With this in mind, let's implement nesting in our code. For example:

.eye-group {
  top: 10px;
  left: 0;
  width: 150px;
  height: 50px;
}
.eye {
  background-color: $black;
  width: 30px;
  height: 50px;
  border-radius: 100%;
}
.eye-left {
  left: 15px;
}
.eye-right {
  right: 15px;
}
Enter fullscreen mode Exit fullscreen mode

becomes:

.eye-group {
  top:10px;
  left:0;
  width: 150px;
  height: 50px;
  .eye {
    background-color: $black;
    width: 30px;
    height: 50px;
    border-radius: 100%;
    &.eye-left {
      left:15px;
    }
    &.eye-right {
      right:15px;
    }
  }  
}
Enter fullscreen mode Exit fullscreen mode

Preprocessors are a great asset in building CSS images and will drastically speed up your workflow. They also offer more advanced options like function, loops, mixins etc. These will come in handy in more intricate illustrations, as we'll see in the last part of this series.

:before and :after

Until now, we’ve created a div for each shape in our smiley face. While this is totally fine, there is a way to reduce the number of HTML elements. This is where the pseudo-selectors :before and :after come in.

These pseudo-selectors come automatically with any HTML element you create. So for any element in your HTML, you’ll be given two extra containers within which to add content. Although they are not initially present in the DOM, you can target them with the content property, like so:

.some-class:before, .some-class:after { 
  content: "";
}
Enter fullscreen mode Exit fullscreen mode

This property can be used to insert content such as text or images. In CSS illustrations, we don't want any of that, but we still want to use the selector, which we can do by using empty quotes. While it might look like a unnecessary step, this is the property that will ensure these selectors are available, so make sure it's there. A good way to not forget it, as well as avoiding repetition, is to assign it to all :after and :before elements by default:

*:before, *:after {
  position: absolute;
  content: '';
}
Enter fullscreen mode Exit fullscreen mode

As you would expect, :before and :after depend on their parent for size and positioning.

Let's look at how we can use these in our code. Consider the .mouth element. It has a child: the .tongue element. We're going to replace the .tongue selector with an :after pseudo-selector:

.mouth {
  width: 100%;
  height: 75px;
  bottom: 0;
  background-color: $black;
  border: 5px solid $black;
  border-radius: 10px 10px 150px 150px;
  overflow: hidden;
  &:after {
    background-color: $tongue-color;
    width: 100px;
    height: 80px;
    border-radius: 50%;
    left:25px;
    top:30px;
  }
}
Enter fullscreen mode Exit fullscreen mode

The same logic can be applied to replace the .pupil elements:

.eye-group {
  top: 10px;
  width: 150px;
  height: 50px;
  .eye {
    background-color: $black;
    width: 30px;
    height: 50px;
    border-radius: 100%;
    border: 5px solid $black;
    &:after {
      width: 10px;
      height: 15px;
      top: 5px;
      background-color: #FBD671;
      border-radius: 50%;
    }
    &.eye-left {
      left: 15px;
    }
    &.eye-right {
      right: 15px;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we can get rid of the .tongue and .pupil divs in the html:

<div class="head">
  <div class="face">
    <div class="mouth"></div>
    <div class="eye-group">
      <div class="eye eye-left"></div>
      <div class="eye eye-right"></div>
    </div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

While this isn’t absolutely necessary, I find this practice helps to keep my code cleaner, and avoids overcrowding my HTML. Again, only use pseudo-selector when it makes sense to do so. (eg: to create an element visually related to its parent container).

HTML templating language

Another step we can take to improve our code is to use an HTML templating language. I like to use Pug, which is also built in to the Codepen editor.

Here’s how it works: Pug uses tag names to represent a full HTML element. For example, <div></div> will simply translate to div in Pug. It uses indentation to represent nesting, recreating the tree structure of HTML.

In CSS images, it is recommended to use div elements only, and assign them a class to be able to easily target them. The way to represent a div with a class in Pug is as follows:

Normal html:

<div class="container"></div>
Enter fullscreen mode Exit fullscreen mode

Pug:

div.container
Enter fullscreen mode Exit fullscreen mode

Or, because div is the default tag name, it can be omitted:

.container
Enter fullscreen mode Exit fullscreen mode

I find this syntax to be much cleaner, and it allows me to focus on the class. Since I already know that all my elements are divs, I don't need any useless syntax polluting my code.

Let's convert our HTML to Pug:

.head
  .face
    .mouth
    .eye-group
      .eye.eye-left
      .eye.eye-right
Enter fullscreen mode Exit fullscreen mode

Much cleaner.

Similarly to CSS preprocessors, there are many things you can do with Pug as it is powered by JavaScript: mixins, includes etc. When it comes to CSS images, one of the most powerful features is loops. We'll see a bit later how to use them.

Here's the final project in CodePen. You can see that none of our latest changes have affected the illustration at all.

Right, we’ve learned a lot already! How to create various shapes and assemble them into a cohesive illustration, how to use hacks to emulate more complex shapes, how to use preprocessors, templating languages and pseudo-selectors to improve our workflow and clean up our code. In the second part of this series, we'll practice these new concepts and build a CSS Polaroid. Then, we'll learn how to animate it.
 
Part 2: Intro to CSS animations with a CSS Polaroid

Latest comments (48)

Collapse
 
utkarshkalra profile image
utkarsh

thanks a lot, it really did clear a lot of my doubts

Collapse
 
hills_ko profile image
Hill Onyechekwa

i don't know but i don't think the way you use :after in this project works. Tried it but the every time i change the .tongue and .pupil selectors to :after the vanish. if you can explain why that will be great.

Collapse
 
__shahidshaikh profile image
Shahid Shaikh

Thank you so much for this well detailed article. I always wanted to design icons using pure CSS and tried a few times as well but it always seemed to be very hard. This article has been very helpful. Here's what I made: codepen.io/shahidshaikhs/pen/pojrrxX

Collapse
 
__shahidshaikh profile image
Shahid Shaikh

Thank you so much for this well detailed article. I always wanted to design icons using pure CSS and tried a few times as well but it always seemed to be very hard. This article has been very helpful. Here's what I made: codepen.io/shahidshaikhs/pen/pojrrxX

Collapse
 
astongemmy profile image
Aston Gemmy

You're amazing! Thanks for the easy-to-understand method you've adopted. So clear to understand.

Collapse
 
kokaneka profile image
kapeel kokane

Thanks a lot for this! Getting into css illustrations lately and this would help a lot!

Collapse
 
vinuch profile image
vincent edeh

wow this is so cool

Collapse
 
judecodes profile image
Cool

Hey I was wondering on where to get some inspirations for illustration?

Collapse
 
bytrangle profile image
Trang Le

This is so inspiring, Agathe. I used to wonder what's the point of all those CSS illustrations on Codepen. Visually dazzling, yes, but I didn't get the point. Then recently I started playing with making CSS icons, and it dawned on me: It doesn't need to have a point. I have discovered aspects of CSS that I never knew, or never got the opportunity to practice. I'm excited by what cool drawings can be made with pure codes, and that's enough.

Collapse
 
codealexx profile image
alex

Nice post, at some point it's a lot easier to use display:flex; or stuff like justify-content:space-between (could've used that for .eye-group instead of positioning both eyes with left: 15px; and right:15px;) though.

.eye-group {
width:150px;
height:50px;

position:absolute;
top:10px;left:0;

display:flex;
justify-content:space-between;
padding: 0 15px;
box-sizing:border-box;

}

Collapse
 
irwingb profile image
irwingb

Many thanks for this wonderful article

Collapse
 
robertcoopercode profile image
Robert Cooper

Very well structured article showing how we can incrementally improve on the CSS image 👍🏼

Collapse
 
bay007 profile image
egalicia

I share my implementation: codepen.io/bay007/pen/NEzwGW :D
Thanks.

Collapse
 
ardhichoiruddin profile image
Ardhi Choiruddin

cool

Collapse
 
negarjf profile image
Negar

Hey Agathe,
Thank you for your great article.
Just one thing I've noticed while creating the "mouth", in Codepen. I think "box-sizing" should be set to "border-box". Otherwise, the width of the mouth would exceed the face container. Please correct me if I'm wrong.