Alvaro Montoro
Posted on November 30, 2023
In this article, we will see how to make a responsive cartoon of Santa Claus using HTML and CSS. Like this one:
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>
.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%);
}
Note: The canvas must have a relative or absolute position to place the different elements where we want.
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>
.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%);
}
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>
.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);
}
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 acontent
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>
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);
}
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>
.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%)
}
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>
.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);
}
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>
.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);
}
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>
.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%;
}
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 tobackground-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%;
}
/* ... */
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;
/* ... */
}
/* ... */
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;
}
}
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>
And with that, we are done!
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 */
/* ... */
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:
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:
Posted on November 30, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.