DEV Community

Cover image for CSS Art: Drawing Santa Claus in CSS
Alvaro Montoro
Alvaro Montoro

Posted on • Originally published at alvaromontoro.com

CSS Art: Drawing Santa Claus in CSS

In this article, we will see how to make a responsive cartoon of Santa Claus using HTML and CSS. Like this one:

Our Santa will be slightly different from this one… but better!

We will do it step by step, explaining each shape and each decision (or almost of all of them). Because, after all, the image is just a combination of elements with different shapes. (Read more about shapes on CSS.)

Let’s start by…

Setting the canvas

By canvas, I don’t mean a <canvas> element, but a canvas in which to do our painting. This will be helpful because we can use it as a reference for our elements once it is set.

If we use relative units for canvas and content, we will be actually creating a responsive image with CSS. That’s the reason why most of the units used in this drawing are going to be %.

As a drawing helper, we can add a repeating-linear-gradient to create a background grid which will be useful for positioning elements:

<div class="canvas">
</div>
Enter fullscreen mode Exit fullscreen mode
.canvas {
  width: 80vmin;
  height: 80vmin;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  border: 1px solid #ddd;
  background-image: 
    repeating-linear-gradient(transparent 0 9.85%, #ddd 0 10%),
    repeating-linear-gradient(to right, transparent 0 9.85%, #ddd 0 10%);
}
Enter fullscreen mode Exit fullscreen mode

Note: The canvas must have a relative or absolute position to place the different elements where we want.

Grid

The lines will help for positioning reference, and we’ll remove them later
 

For the colors, we are going to use CSS variables. They will allow us to have consistent colors and will facilitate changes.

Drawing the head

The head will be multiple circles and ellipses: a big circle for the face, smaller circles for the eyes, and ellipses for the cheeks.

To round objects and make them circular or elliptical, we use border-radius with a value of 50% or higher.

In the original version of this article, I had the eyes and cheeks outside the face, then their size and position was relative to the canvas, which was a small issue if I wanted to change the position or size of the face elements. For this version, I will place them inside the face, so things are easier.

We create a circle for the face, another for one eye, and an ellipse for one of the cheeks. Also on the original, I used box-shadow to duplicate the second eye and cheek, but box-shadow needs a unit different than % which made the drawing only partially responsive. Duplicating the eye and cheek will make it more robust and maintainable (it’s funny talking like that about a drawing, huh?)

<div class="canvas">
  <div class="head">
    <div class="cheek"></div>
    <div class="cheek"></div>
    <div class="eye"></div>
    <div class="eye"></div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode
.canvas {
  --skin: #fca;
  --eyes: #630a;
  --cheeks: #f001;
  /* ... */
}

/* ... */

.head {
  --positionX: 28%;
  --positionY: 63%;
  position: absolute;
  top: 10%;
  left: 50%;
  border-radius: 50%;
  width: 25%;
  height: 25%;
  transform: translate(-50%, 0);
  background: var(--skin);
}

.eye {
  position: absolute;
  top: var(--positionY);
  left: var(--positionX);
  width: 12%;
  height: 12%;
  background: var(--eyes);
  border-radius: 50%;
}

.eye + .eye {
  left: auto;
  right: var(--positionX);
}

.cheek {
  position: absolute;
  top: calc(var(--positionY) + 7%);
  left: calc(var(--positionX) - 12%);
  width: 20%;
  height: 12%;
  background: var(--cheeks);
  border-radius: 50%;
}

.cheek + .cheek {
  left: auto;
  right: calc(var(--positionX) - 12%);
}
Enter fullscreen mode Exit fullscreen mode

Grid with circles drawn inside

Is that a face?
 

The beard and mustache

In this section, we will use two basic CSS shapes that are really useful for drawing: the oval and eye shapes for the beard and mustaches, respectively.

The beard will go behind the head. To achieve the oval shape, we take advantage of border-radius taking two values separated by /: one for the horizontal axis and another for the vertical.

Note: when we say that the border-radius takes two values, each value can have 1–4 sub-values… which may make it look like there are 8 values. By two values, we mean two sets of up to 4.

The mustaches will go on top of the head. There will be two elements with basically the same styles (just different rotation) next to each other. The adjacent sibling combinator (+) will go perfect for this.

<div class="canvas">
  <div class="beard"></div>
  <div class="head">
    <div class="cheek"></div>
    <div class="cheek"></div>
    <div class="eye"></div>
    <div class="eye"></div>
    <div class="mustache"></div>
    <div class="mustache"></div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode
.canvas {
  --beard: #eee;
  --mustache: #fff;
  /* ... */
}

/* ... */

.beard {
  position: absolute;
  top: 10%;
  left: 50%;
  width: 30%;
  height: 40%;
  background: var(--beard);
  transform: translate(-50%, 0);
  border-radius: 100% / 120% 120% 80% 80%;
}

.mustache {
  position: absolute;
  top: 88%;
  left: 52%;
  width: 40%;
  height: 40%;
  background: var(--mustache);
  border-radius: 100% 10% 100% 0;
  transform-origin: top right;
  transform: translate(-100%, 0) rotate(25deg);
}

.mustache + .mustache {
  left: 48%;
  border-radius: 10% 100% 0 100%;
  transform-origin: top left;
  transform: rotate(-25deg);
}
Enter fullscreen mode Exit fullscreen mode

Grid with a cartoon of a bearded face

Our Santa Claus starts looking like Santa already.
 

Drawing the hat

The hat is going to be a single element, but it will include two pseudo-elements: ::before and ::after.

This is convenient because their size and position will be relative to the hat, and changing a single component will update all three of them at once. We could have 3 elements (hat, base, and pompom), but this way, we practice pseudo-elements.

Important: ::before and ::after must have a content property or they won't be displayed. It's ok if the value is empty, but it needs to be there.

The hat is a basic square in which one corner (top left) has a border-radius of 100%, creating a nice curvature. The pompom is just a circle. So the trickiest part is the bottom of the hat.

For the bottom, we are going to use a shape I call a pipe. We draw it by having a square and adding two values for the border-radius: 100% / 50%. This way, the square's top and bottom will be curved while the sides will be flat.

Once we have that shape, we add an radial gradient as background. Then we’ll have that curved bottom. We may need to rotate it a little to adjust it to the head:

<div class="canvas">
  <div class="beard"></div>
  <div class="head">
    <div class="cheek"></div>
    <div class="cheek"></div>
    <div class="eye"></div>
    <div class="eye"></div>
    <div class="mustache"></div>
    <div class="mustache"></div>
    <div class="hat"></div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode
html {
  background: #bcd;
}

.canvas {
  --suit: #d00;
  /* ... */
}

/* ... */

.hat {
  position: absolute;
  width: 98%;
  height: 80%;
  background: var(--suit);
  border-radius: 100% 20% 0 0;
  top: -40%;
  left: 50%;
  transform: translate(-50%, 0) rotate(1deg);
}

.hat::before {
  content: "";
  display: block;
  position: absolute;
  bottom: -17%;
  left: -5%;
  width: 110%;
  height: 40%;
  border-radius: 100% / 50%;
  transform: rotate(-2deg);
  background: 
    radial-gradient(200% 100% at 50% 100%, #0000 30%, var(--mustache) 31%);
}

.hat::after {
  content: "";
  display: block;
  position: absolute;
  right: -25%;
  top: -15%;
  width: 40%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--beard);
}
Enter fullscreen mode Exit fullscreen mode

Grid with a cartoon of santa's head

We added a blue-ish background so we can see the bottom of the hat and the pompom.
 

The body

The body shape is like a bell, which is basically an oval with small bottom corner radii in CSS. You can read more about shapes in CSS on this article I published here.

But that’s not the interesting part about the body. We are going to draw the belt and the buttons section using CSS gradients: radial-gradient() and linear-gradient() respectively.

The buttons section is a simple left-to-right linear gradient using three colors: transparent, then white, and then transparent again. Leaving a small % between the colors to add some “blurry” effect.

The belt is a bit trickier: it is a circular (radial) gradient, and we’ll have to play with the values to position it exactly where we want. It follows a similar transparent-color-transparent pattern as the buttons section:

<div class="canvas">
  <div class="body"></div>
  <div class="beard"></div>
  <div class="head">
    <div class="cheek"></div>
    <div class="cheek"></div>
    <div class="eye"></div>
    <div class="eye"></div>
    <div class="mustache"></div>
    <div class="mustache"></div>
    <div class="hat"></div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode
.canvas {
  --belt: #222;
  /* ... */
}

/* ... */

.body {
  position: absolute;
  top: 35%;
  left: 50%;
  width: 50%;
  height: 50%;
  background: var(--suit);
  border-radius: 100% / 150% 150% 25% 25%;
  transform: translate(-50%, 0);
  background-image:
    radial-gradient(circle at 50% -50%, transparent 75%, var(--belt) 75.1% 83%, transparent 83.1%),
    linear-gradient(to right, transparent 42%, white 42.2% 57%, transparent 57.2%)
}
Enter fullscreen mode Exit fullscreen mode

Cartoon of santa's head and body

The bell shape is super useful for drawing the body of characters
 

Notice how the gradients in the body don’t start exactly where the previous one ends, but they have a small decimal added to them. This is because browsers make the cut too sharp for CSS gradients (not for SVG), and that small decimal helps smooth the edges.

With that, we have the body. But it looks a bit boring. It’s time to improve it by…

Adding details to the body

The first detail is going to be the buttons. It is going to be a single rounded element with different shadows. We used a similar technique for the eyes, but instead of horizontally, the shadows will go vertically.

The belt buckle is just a rectangle! We add golden borders, a little bit of border-radius (we don’t want an ellipse). The background will also be gold, but with an inset box-shadow, we highlight the prong.

Checking some Santa drawings, many have the bottom of Santa’s jacket as white. So we expand the radial-gradient from the body, so it ends in white instead of transparent:

<div class="canvas">
  <div class="body">
    <div class="belt"></div>
  </div>
  <div class="beard"></div>
  <div class="head">
    <div class="cheek"></div>
    <div class="cheek"></div>
    <div class="eye"></div>
    <div class="eye"></div>
    <div class="mustache"></div>
    <div class="mustache"></div>
    <div class="hat"></div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode
.canvas {
  --belt-buckle: gold;
  /* ... */
}

/* ... */

.body {
  position: absolute;
  top: 35%;
  left: 50%;
  width: 50%;
  height: 50%;
  background: var(--suit);
  border-radius: 100% / 150% 150% 25% 25%;
  transform: translate(-50%, 0);
  background-image:
    /* buttons */
    radial-gradient(circle at 50% 36%, var(--belt) 2.75%, #0000 3%),
    radial-gradient(circle at 50% 48%, var(--belt) 3%, #0000 3.25%),
    radial-gradient(circle at 50% 60%, var(--belt) 2.75%, #0000 3%),
    radial-gradient(circle at 50% 90%, var(--belt) 2.25%, #0000 2.5%),
    /* belt */
    radial-gradient(circle at 50% -50%, transparent 75%, var(--belt) 75.1% 83%, transparent 83.1%),
    /* flap */
    linear-gradient(to right, transparent 42%, white 42.2% 57%, transparent 57.2%);
  clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 51% 100%, 50% 96%, 49% 100%, 0% 100%);
}

.belt {
  position: absolute;
  top: 75%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 23%;
  height: 15%;
  background: 
    linear-gradient(var(--belt-buckle) 0 0) 75% 50% / 25% 12% no-repeat,
    linear-gradient(var(--belt) 0 0) 50% 50% / 85% 80% no-repeat,
    var(--belt-buckle);
}
Enter fullscreen mode Exit fullscreen mode

Santa's head and body with details

Not bad… but something is missing
 

Notice how the buttons have different sizes, but they look visually the same size. This is because the size is calculated from the farthest corner to the button, so if we set the same percentage for all of them, they will all have different sizes.

As a final step, we added a clip-path to snip the bottom of the buttons section, so it looks like the jacket overlaps.

Arms and hands

The arms will be a single element with the same shape as the body: a bell. But this bell is going to be shorter and wider. That way, when we place it behind the body, it will “overflow” on the sides.

Adding a small vertical gradient from transparent to semi-transparent black, the arms get some color distance from the body. The gradients make it look like a shadow and emphasizes the backward position.

The hands are a simple circle again. Same routine as the eyes or the buttons. I could have gone for something slightly more complicated (or even an ellipse), but I have to confess I’m terrible at drawing hands… so the circle will do:

<div class="canvas">
  <div class="hand"></div>
  <div class="hand"></div>
  <div class="arms"></div>
  <div class="body">
    <div class="belt"></div>
  </div>
  <div class="beard"></div>
  <div class="head">
    <div class="cheek"></div>
    <div class="cheek"></div>
    <div class="eye"></div>
    <div class="eye"></div>
    <div class="mustache"></div>
    <div class="mustache"></div>
    <div class="hat"></div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode
.arms {
  position: absolute;
  top: 37%;
  left: 50%;
  transform: translate(-50%, 0);
  width: 65%;
  height: 40%;
  background: #a00;
  border-radius: 100% / 170% 170% 25% 25%;
  background-image: linear-gradient(transparent 20%, #0003);
}

.hand {
  --positionX: 18%;
  position: absolute;
  top: 70%;
  left: var(--positionX);
  width: 13%;
  height: 13%;
  background: var(--belt);
  border-radius: 50%;
}

.hand + .hand {
  left: auto;
  right: var(--positionX);
}
Enter fullscreen mode Exit fullscreen mode

Santa Claus head and torso

Kind of cute Santa already
 

Now the top part of our Santa is complete. This would even make a cute element for a website (e.g., animating it up from the bottom of the page.)

Drawing the legs

The legs will have two parts: the leg in itself and the boot tip (only the tip, because the boot will be drawn with a linear gradient on the leg itself).

We draw rectangles for the legs, distance them a little (using the adjacent sibling combinator that we used before for the mustaches), add a red-black gradient to separate pants and boots… and slightly tilt them with skew(), so they don't look too symmetrical.

Finally, for the boot tip, we use the ::after pseudo-element, rounding the top corners:

<div class="canvas">
  <div class="hand"></div>
  <div class="hand"></div>
  <div class="arms"></div>
  <div class="leg"></div>
  <div class="leg"></div>
  <div class="body">
    <div class="belt"></div>
  </div>
  <div class="beard"></div>
  <div class="head">
    <div class="cheek"></div>
    <div class="cheek"></div>
    <div class="eye"></div>
    <div class="eye"></div>
    <div class="mustache"></div>
    <div class="mustache"></div>
    <div class="hat"></div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode
.leg {
  position: absolute;
  top: 75%;
  left: 29%;
  width: 19%;
  height: 25%;
  background: var(--suit);
  transform: skew(2deg);
  background-image: linear-gradient(#0002, transparent 70%, var(--belt) 0);
}

.leg + .leg {
  left: 52%;
}

.leg::after {
  content: "";
  display: block;
  position: absolute;
  bottom: 0;
  left: -6%;
  width: 110%;
  height: 20%;
  background: black;
  border-radius: 50% / 100% 100% 0 0;
}

.leg + .leg::after {
  left: -4%;
}
Enter fullscreen mode Exit fullscreen mode

Cartoon of Santa Claus

And we are done!
 

Adding the ground and some snow

This step is optional. I am adding it as it is a “standalone drawing”, but you don’t have to do it.

Let’s start with the ground because it is easier. We don’t even need a new element! We can use the ::before pseudo-element of the canvas.

We will make it really big. So big that it will overflow the viewport, and we will need to add overflow: hidden to the document's <body> to avoid annoying scrollbars.

Then we will place it at the bottom of the canvas and add a tiny curvature to it (by making it an inverted bell!) And just like that, we have our Santa standing on a hill.

The snow is also a fairly simple step. We will create it by adding a bunch of radial gradients to the <body>, each of them a background image with different sizes (so they seem more irregular).

Note: background-image allows for more than one value as long as they are separated by commas. Same principle applies to background-position, background-repeat, background-size...

The result looks like this:

html {
  background: #abc;
  overflow: hidden;
  background-image:
    radial-gradient(circle at 50% 50%, white 2.5%, transparent 0),
    radial-gradient(circle at 30% 90%, white 1.5%, transparent 0),
    radial-gradient(circle at 70% 10%, white 1%, transparent 0),
    radial-gradient(circle at 10% 40%, white 1%, transparent 0);
  background-size: 45vmin 35vmin, 50vmin 70vmin, 60vmin 50vmin, 65vmin 60vmin;
  background-position: 0 0;
}

.canvas::before {
  content: "";
  display: block;
  position: absolute;
  top: 90%;
  left: 50%;
  width: 200vmax;
  height: 200vmax;
  background: white;
  transform: translate(-50%, 0) rotate(1deg);
  border-radius: 100% / 20%;
}

/* ... */
Enter fullscreen mode Exit fullscreen mode

CArtoon of Santa Claus in a snowy field

A Christmas scene with CSS. Some minor details left
 

And with that, our drawing is complete. We need to clean up a little, removing the grid that served as guidelines.

Animating the scene

We have complete a static drawing… but we can add some animations to make it pop up:

  • Santa could blink as a normal person does
  • Moving the mustaches now and then shaking that cold
  • It would be a lot cooler if the snow actually fell

Blinking is a simple animation, making the eye’s height from whatever it is to zero and then back. We may need to add a vertical translation for a better experience.

Moving the mustaches is also a simple animation, but it starts getting “messy” because we need to synchronize both mustaches (remember it is a two-part thing!) Playing with the times and the angles will yield great results.

As for the snow, we can animate the background-position to make it look like it's falling. Doing it vertically is easy, but it doesn't look too realistic. Snow zigzags when falling, so we will add that zigzagging to our animation.

Note: the real challenge with animation is timing. If all the animated parts have the same timing functions and last for the same amount of time, the animation looks fake. Mixing things make it look more natural and nicer… Unfortunately, I’m not that great at it. For reference, you can check the work on Adam Kuhn, Sarah Drasner, or Jhey Tompkins. They are really amazing.

With all the animations listed above, our animated cartoon looks nicer:

@keyframes snow {
  0% { background-position: 0 0, 0 0, 0 0, 0 0; }
  40% { background-position: 10px 14vmin, -20px 28vmin, 20px 20vmin, 10px 24vmin; }
  60% { background-position: -10px 21vmin, -30px 42vmin, 30px 30vmin, 15px 36vmin; }
  100% { background-position: 0 35vmin, 0 70vmin, 0 50vmin, 0 60vmin; }
}

@keyframes blink {
  0%, 6%, 100% { height: 12%; }
  3% { height: 0%; }
}

@keyframes moveMustache {
  0%, 40%, 44%, 100% { transform: translate(-100%, 0) rotate(25deg); }
  42% { transform: translate(-100%, 0) rotate(30deg); }
}

@keyframes moveMustache2 {
  0%, 40%, 44%, 100% { transform: rotate(-25deg); }
  42% { transform: rotate(-30deg); }
}

html {
  animation: snow infinite 7s linear;
  /* ... */
}

.eye {
  animation: blink 5s infinite linear;
  /* ... */
}

.mustache {
  animation: moveMustache 7s infinite linear;
  /* ... */
}

.mustache + .mustache {
  animation: moveMustache2 7s infinite linear;
  /* ... */
}

/* ... */
Enter fullscreen mode Exit fullscreen mode

It is important to consider the will of the user and disable the animations if they chose so (especially taking into account accessibility). We can do that with the prefers-reduced-motion media feature.

@media (prefers-reduced-motion) {
  * {
    animation: none !important;
  }
}
Enter fullscreen mode Exit fullscreen mode

This selector is too generic. If you incorporate this drawing of Santa on a website, you may want to adapt it so it doesn’t impact other animations on your page.

Adding alternative text

CSS Art is a drawing but it cannot have alternative text… or can it? Actually, it can! And it would be a nice thing to add if we want assistive technologies to identify our Santa drawing as a picture with a description.

I wrote an article about how to make CSS Art more accessible. In this case, we’d need to add a role of img at the root of our drawing and an aria-label also at the root:

<div class="canvas" 
     role="img" 
     aria-label="Cartoon of Santa Claus on top of a snowy hill">
  <!-- ... -->
</div>
Enter fullscreen mode Exit fullscreen mode

And with that, we are done!

Cartoon of Santa standing in a snowy field

Our drawing!
 

Feel free to add more details: eyebrows would be nice, some hair coming out from below the hat, some gifts around Santa, maybe even a reindeer popping up somewhere!

A final note on responsiveness

As it is right now, the drawing is inside the .canvas root element which has a width and height of 80vmin. As vmin is a responsive unit (it depends on the size of the view frame), the drawing will adapt to the screen, but that may not be what we want.

We may want to add the drawing into specific space within the page, and then the vmin unit would be an issue. Don’t worry. Let’s make a small change to fix that:

.canvas {
  width: 80vmin;
  aspect-ratio: 1;
  /* remove the height */
  /* ... */
Enter fullscreen mode Exit fullscreen mode

By removing the height and specifying an aspect-ratio of 1, the canvas will always be squared. And because we used percentages in all the sizes and backgrounds, we can change the width to whatever we want and the drawing will scale nicely. For example, here it is after setting the width to 200 pixels:

Cartoon of a small Santa standing in a snowy field

Same Sant Claus drawing, just smaller.
 

Conclusion

And that’s it! We have created an animated scene with Santa Claus and, during this process, we have practiced a lot of CSS:

  • Animations
  • Aspect-ratio
  • Backgrounds
  • Border-radius
  • Box-shadow
  • Combinators
  • Gradients
  • Overflow
  • Positioning
  • Pseudo-elements
  • Transforms
  • Units & colors
  • Variables

You can see the code of the final demo on this CodePen:

Top comments (5)

Collapse
 
kehoecj profile image
Clayton Kehoe

Can’t believe this is possible in CSS - great job!

Collapse
 
siriustl profile image
Sirius-TL

cool

Collapse
 
mezieb profile image
Okoro chimezie bright

Wow nice work and thanks for sharing

Collapse
 
behainguyen profile image
Be Hai Nguyen

WOW! Thank you.

Collapse
 
madsstoumann profile image
Mads Stoumann

It takes practice to learn CSS art, but it takes talent to do this!
You’re a great illustrator — I love the style and the beautiful shades of red and black.