DEV Community

Cover image for Building an interactive form: CSS image
Alvaro Montoro
Alvaro Montoro

Posted on

Building an interactive form: CSS image

Note: There are unlimited ways of drawing a character. This is my character and my way to draw it. It is simple to animate and easy to draw (which is convenient for a tutorial). You can make a character as complex and complete as you like, and using the technology that you want. Own your character, don't limit yourself to this example.

In this post, we will explain how to draw a cartoon face in CSS while answering any question that may pop up. Like for example...

Why a CSS image and not an SVG?

That is a great question -thanks for asking-, unfortunately, there's not a clear-cut answer. SVG is better for drawing than CSS, mainly because SVG is designed for drawing and CSS is not... but using CSS for simple drawings has some advantages:

  • It doesn't require any additional learning (still you should learn SVG because it is interesting).
  • It is lightweight and scalable (as SVG is).
  • Some browsers don't fully support the animation of SVGs (it would require a library like GreenSock)

Also, a selfish reason: it is my tutorial! I want to keep it simple and not introduce new concepts. And I like playing with CSS drawings so making a small one for this tutorial is fine :)

Let's get back on track...

Designing the character

The person is going to be a vectorial looking character made primarily of circles and rectangles (check this article for the many shapes of CSS):

  • The neck and shirt are going to be rectangles
  • The body is going to be an ellipse
  • And everything else is basically going to be a circle: head, ears, eyes... even the mouth!

Note: add eyebrows to your character. They give a ton of expression and personality to the drawing (even more when animated). I kind of regret not adding them to mine.

Here it is a schema of what we are going to do:

Schema of the CSS character with the different parts highlighted and explained

And here it is the HTML code for it:

  <figure aria-hidden="true">
    <div class="person-body"></div>
    <div class="neck skin"></div>
    <div class="head skin">
      <div class="eyes"></div>
      <div class="mouth"></div>
    <div class="hair"></div>
    <div class="ears"></div>
    <div class="shirt-1"></div>
    <div class="shirt-2"></div>
Enter fullscreen mode Exit fullscreen mode

We added aria-hidden="true" to the figure because it is only decorative and we want it to be ignored by screen readers (it didn't have any content to read, but just in case).

The different parts of our character are organized in order from back to front: the person's body will be in the background and the shirt will be on top. And everything will be wrapped in a circle (the figure) with hidden overflow.

Drawing the character

We will not speak about everything in the CSS drawing (you can see the code in the fiddle below), but some parts may be interesting or need a deeper explanation, and we will dive into them.

Setting the "canvas"

We use the figure as the "canvas" for our drawing, make it circular, and hide the overflow. Also, we set the position to relative so we can position the different elements in an absolute way (something that we set right below).

Here is the CSS for the figure:

figure {
  --skinH: 30;
  --skinS: 100%;
  --skinL: 87%;
  --hair: rgb(180, 70, 60);
  background: hsl(var(--fgColorH), calc(var(--fgColorS) * 2), 95%);
  border: 1px solid rgba(0, 0, 0, 0.0625);
  border-radius: 50%;
  height: 0;
  margin: auto auto;
  margin-bottom: 2rem;
  order: 1;
  overflow: hidden;
  padding-top: 60%;
  position: relative;
  width: 60%;

figure div {
  position: absolute;
  transform: translate(-50%, -50%);
Enter fullscreen mode Exit fullscreen mode

We double down in the use of HSL and CSS variables. This may be convenient for things like changing the color of the skin for example. the character is white, but you could easily change the skin tone by updating the light (a smaller percentage means darker skin) or adjusting the hue (beware as this could lead to strange results too.)


All the other elements are drawn directly using an element and giving it shape, but the hair is special.

In this case, the shape is used to limit the hair, and a pseudo-element is used to draw the hair itself with a box-shadow (for the right and left sides):

figure .hair {
  top: 40%;
  left: 50%;
  width: 66.66%;
  height: 66.66%;
  border-radius: 100%;
  overflow: hidden;

figure .hair::before {
  background: var(--hair);
  border-radius: 50%;
  box-shadow: 4rem 0 var(--hair);
  content: "";
  display: block;
  height: 100%;
  left: -50%;
  position: absolute;
  top: -60%;
  width: 100%;
Enter fullscreen mode Exit fullscreen mode

Again CSS variables to be able to easily change the hair color just by changing a small part of the code.


Something is interesting about the mouth. Many people that draw in CSS, if they want a rounded line they will draw a circle, and only add one border. That is perfectly fine, but it has some issues: if no other borders are specified, they will be zero, and our line will become thinner at the edges.

I honestly don't like that. Instead, I prefer to add a transparent border to the element, and then change the color of the side I want to use. That way the line will always have the same width (although it may end too "abruptly" for some people's liking.)

figure .head .mouth {
  border: 0.125rem solid transparent;
  border-bottom: 0.125rem solid var(--borderDarker);
  width: 25%;
  border-radius: 50%;
  transition: all 0.5s;
  top: 75%;
  left: 50%;
  height: 10%;
Enter fullscreen mode Exit fullscreen mode


For the shirt, we use two skewed rectangles, one opposite to the other. The code for this is not especially difficult or interesting, but it may be worth a look if you haven't combined selectors in CSS before, or to see how you can apply multiple transforms in one property:

figure .shirt-1,
figure .shirt-2 {
  width: 12%;
  height: 7%;
  background: hsl(var(--bgColorH), var(--bgColorS), var(--bgColorL));
  top: 76%;
  left: 36.5%;
  transform: skew(-10deg) rotate(15deg);

figure .shirt-2 {
  left: 52.5%;
  transform: skew(10deg) rotate(-15deg);
Enter fullscreen mode Exit fullscreen mode

Animating the eyes

So far our character is static: it doesn't move at all, and it looks like a still image. To change this, we are going to add some CSS animation to the mix. We will make our character blink. It is something simple -readjusting the height of the eyes-, but it will bring our little drawing to life.

This is the CSS for the blinking animation:

@keyframes blink {
  0%, 90%, 100% {
    height: 10px;
  95% {
    height: 0px;

figure .head .eyes::before,
figure .head .eyes::after {
  animation: blink 5s infinite;
Enter fullscreen mode Exit fullscreen mode

You may say now: "Hey! you use percentages and rem everywhere, why is the animation using pixels?" And that's another great question. When we tested on Safari, the animation didn't work as expected when using rem, but changing to its px equivalent worked just fine. It could be a bug with Safari with the rem unit on animations or specifically on pseudo-elements' animations. Whatever the case, using pixels will fix it.

With the animated CSS character, the form looks like this on JSFiddle:

We had an ugly but functional form in HTML, then we added styling and a friendly-looking character on top using CSS. It's time to get our hands dirty with JavaScript, and connecting everything... after that, let's give some final touches in CSS to add some fallback interactivity.

Top comments (4)

austin_mesh profile image

Most of the CSS multiple transforms shows error in it's property coding with my phpstorm

alvaromontoro profile image
Alvaro Montoro

That's interesting. Having multiple transforms is definitely valid. What type of error does it show?

austin_mesh profile image

E.g Background (--background);. It shows errors that it's not a css property.

Thread Thread
alvaromontoro profile image
Alvaro Montoro

You need to use var() to read the value of a CSS variable (e.g. background: var(--background);). It seems that code is missing it.