CSS animations are a powerful way to create captivating user experiences online, and their simple API makes the barrier of entry low, allowing beginners to leverage the power of animations with just a few lines of code.
In this tutorial, we’ll take a look at how to create facial expression animations in CSS visualized as an old CRT monitor. As a result, we’ll gain a better understanding of how to use CSS @keyframes
to create animations and use different CSS properties to get the desired result. At the end of this tutorial, we’ll end up with the following:
You can find the source code for this tutorial in one piece on GitHub.
The first thing we need to do is create the HTML layout for the animation. We’ll only need the following three elements:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🧑 Facial Expression Animations in CSS</title>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<div class="face">
<div class="screen">
<div class="expression"></div>
</div>
</div>
</body>
</html>
Notice that the document references styles.css. Create this file next to the HTML file.
.face
: The entire monitor that encapsulates the screen and the facial expression. We’ll use ::before
and ::after
elements to add extra visual elements.screen
: The screen where we’ll add the expression..expression
: The expression element will hold the eyes and mouth for the animation. We’ll use ::before
and ::after
elements to create three different elements visually out of one DOM node.
Now that we have the layout in place, let’s take a look at the CSS. To style the .face
element, add the following rules to the CSS file:
body {
background: #333;
font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
}
.face {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 300px;
height: 300px;
display: flex;
align-items: center;
justify-content: center;
background: #CAA277;
border: 5px solid #2B1607;
user-select: none;
cursor: pointer;
}
At this stage, we’ll have a simple brown box at the middle of the page. To align it to the middle, we can use absolute
positioning combined with top
and left
set to 50%
. Note that we offset the element by -50%
on both the x and y-axis to center align the anchor.
To align the screen to the center inside the monitor, we also need to set the display
to flex
with these two alignments set to the center:
align-items
: Set the vertical alignment.justify-content
: Set the horizontal alignment.To add the back of the monitor, we’ll use the ::before
and ::after
element of the .face
element. Extend the CSS with the following rules:
.face::before,
.face::after {
content: '';
position: absolute;
top: -5px;
right: -60px;
width: 50px;
background: #44595E;
border: 5px solid #2B1607;
height: 100%;
}
.face::after {
top: 50%;
right: -115px;
transform: translateY(-50%);
width: 50px;
height: 90%;
}
This will position the ::before
and ::after
elements on the right side of the monitor. For the correct right
position, we need to take the width of the element plus the borders from each side:
50px (width) + 2 * 5px (borders) -> 60px
We can follow the same calculation for the ::after
element, but this time, we have to take the ::before
element into account, so we have to adjust 60px
by another 60px - 5px
. This is because we want the right border of the ::before
element to overlap with the left border of the ::after
element. Otherwise, we would end up with a 10px
border between the two elements.
Now that we have the base element styled, let’s also add the .screen
. For this, all we have to do is set a width
and height
along with some background and border colors. Append the following to the CSS:
.screen {
position: relative;
width: 220px;
height: 220px;
background: #4D3A29;
border: 5px solid #2B1607;
overflow: hidden;
}
Make sure you set overflow
to hidden
, as we’ll have cases where we animate elements outside the screen.
The element will be automatically aligned to the middle. This is because we used flex
for the .face
element with align-items
and flex-direction
set to center
. We also need the inner edge of the monitor, which we can achieve with another ::before
element:
.screen::before {
content: '';
width: 10px;
height: 100%;
display: block;
background: #CAA277;
border-right: 5px solid #2B1607;
}
Now that we have the base elements sorted out, we can start looking into adding expressions. First, let’s make the monitor sleep. For the mouth, we’re going to use a circle, and for the eyes, we’ll use a crescent that we’ll generate through borders. For the mouth, add the following to the CSS:
.expression {
position: absolute;
top: 75%;
left: 50%;
transform: translateX(-50%);
width: 30px;
height: 30px;
border-radius: 100%;
background: #FFF;
}
What this will do is it’ll create a 30px
circle at 75%
from the top of the screen, horizontally in the middle (using left: 50%
and translateX(-50%)
) with a white background. By setting border-radius
to 100%
, we can change the appearance of a square to a circle. To also add the styles for the eyes, add the following lines of code after the .expression
block:
.expression::before,
.expression::after {
content: '';
display: block;
position: absolute;
width: 20px;
height: 20px;
border-radius: 100%;
top: -20px;
left: -30px;
border-bottom: 3px solid #FFF;
transition: all .3s cubic-bezier(.55, 0, .1, 1);
}
.expression::after {
left: auto;
right: -30px;
}
This will generate two additional elements: one 30px
to the left from the mouth, and one 30px
to the right. The crescent is achieved through the border-bottom
property. Here, we use a 100%
border-radius
as well, and by using a single border, we can essentially generate a crescent.
As we want to animate these elements, make sure you define a transition
rule.
We want to complement the current expression with a sleeping animation to demonstrate the power of CSS animations. We’ll add the following animation:
Let’s create the element first, and then we create the animation for it. For the element, we’ll use the ::after
of our .screen
element. Add the following to your CSS to introduce the “Z“:
.face:not(.awake):not(.angry) .screen::after {
content: 'Z';
position: absolute;
top: 45px;
right: 50px;
color: #FFF;
font-weight: bold;
letter-spacing: 6px;
transform: rotate(-27deg);
opacity: 0;
animation: sleep 4s ease-in-out infinite;
}
First, we want to make sure that this element only appears on the screen when none of the other states are active. This can be achieved using the :not
CSS selector. For additional states, we’ll simply use additional classes. We’ll have the following states:
We have this element absolutely positioned at the top-right part of the screen, with some rotating applied. It calls the sleep
animation that we haven’t created yet. The animation property itself has four different values:
sleep
: The name of the animation that we’ll build in a minute.
4s
: The time it’ll take for our animation to play. We’ll complete the animation in four seconds.
ease-in-out
: The easing to use for the animation. Different easings play the animation at a different rate of change. To experiment with easing, I recommend checking out easings.net.
infinite
: The number of times we want to place this animation. To play it indefinitely, we can use infinite
.
To create an animation in CSS, we need to use the @keyframes
keyword with the name of the animation:
@keyframes sleep {
0% { opacity: 0%; }
30% {
opacity: 100%;
right: 30px;
content: 'Z.'
}
60% {
right: 50px;
content: 'Z..'
}
90% {
right: 30px;
}
100% {
top: -30px;
}
}
This will animate different properties at different stages of the animation.
0%
: Initially, we’ll start with a 0% opacity
and slowly fade in the element over 30% of the animation.
30%
: At 30%, the element is fully faded in, and we also shift it to the right by 20px (50px - 30px)
. We can also change the content.
60%
: At 60%, we change the content again and pull the element back to its initial position (50px
from the right).
90%
: We reiterate the same steps for the right position to pull and push the element back and forth, creating a waving animation.
100%: We want the element to move upwards, outside of the screen. We can achieve this by animating the top
property to a negative value. Note that there are no stops in between for this property. We go from the initial position (top: 45px
) to its final position in one step (top: -35px
).
As we have the waving sorted, let’s create two more states to get the hang of CSS animations. In the awake state, we want our element to blink intermittently.
To add the awake state, expand the CSS with the following lines:
/* Mouth */
.awake .expression {
width: 30px;
height: 15px;
border-bottom: 5px solid #FFF;
background: transparent;
}
/* Eyes */
.awake .expression::before,
.awake .expression::after {
width: 15px;
height: 15px;
background: #FFF;
border: 0;
animation: blink 4s cubic-bezier(.55, 0, .1, 1) infinite;
}
We used the same border solution we had for the eyes for the mouth this time. As it originally had a background color, we need to set it to transparent
. For the eyes, we just need to change their size and add a background color to make them round (as we already have a 100% border-radius
applied for the element).
For the blinking animation, we’ll need to create a new @keyframes
. This time, it’s using a custom cubic-bezier
for the easing, which creates a more natural transition. Now, let’s take a look at the blink
animation:
@keyframes blink {
0% { transform: scaleY(1); }
2% { transform: scaleY(.1); }
4% { transform: scaleY(1); }
50% { transform: scaleY(1); }
52% { transform: scaleY(.1); }
54% { transform: scaleY(1); }
56% { transform: scaleY(.1); }
58% { transform: scaleY(1); }
100% { transform: scaleY(1); }
}
The blink
animation is simply an iteration of scaling the eyes back and forth from 1
to .1
. To make the blinking natural and fast, the transition takes place over 2% of the animation. To make the blinking intermittent, we can create one blink from 0% to 4% (close from 0% to 2%, then open from 2% to 4%) and two others from 50% to 58%. To create a seamless transition, make sure you return to the initial state at 100%.
To finish CSS animations, we’ll create another angry state that will make the monitor shake. But first, let’s turn the eyes inwards with some extra styles to better convey the expression. This can be done with simple rotation rules:
.angry .expression::before,
.angry .expression::after {
top: -30px;
width: 25px;
height: 10px;
}
.angry .expression::before {
transform: rotate(10deg);
}
.angry .expression::after {
transform: rotate(-10deg);
}
.angry {
animation: shake 1s linear infinite;
}
We want the shake animation to play smoothly, so this time, we’ll use a linear
easing. Let’s take a look at what the shake animation looks like:
@keyframes shake {
0% { transform: translate(-50%, -50%) rotate(0deg); }
10% { transform: translate(-50%, -50%) rotate(15deg); }
20% { transform: translate(-50%, -50%) rotate(-15deg); }
30% { transform: translate(-50%, -50%) rotate(15deg); }
40% { transform: translate(-50%, -50%) rotate(-15deg); }
50% { transform: translate(-50%, -50%) rotate(15deg); }
60% { transform: translate(-50%, -50%) rotate(-15deg); }
70% { transform: translate(-50%, -50%) rotate(15deg); }
80% { transform: translate(-50%, -50%) rotate(-15deg); }
90% { transform: translate(-50%, -50%) rotate(15deg); }
100% { transform: translate(-50%, -50%) rotate(0deg); }
}
As the entire element has a default translate
property set to -50%
on both the x and y-axis, we need to pass it alongside the rotate
rule to prevent the element from losing the translate
rule. All that happens here is that the element is rotated back and forth between -15°
and 15°
, creating the shaking effect. By increasing or decreasing the degree amount, we can make the effect more subtle or amplify it.
Don’t forget to always return to the initial state at 100% to create a smooth loop.
Now that we have all states created in CSS, there’s only one thing missing, and that is to actually change the states interactively. For this, we’ll use a simple click event listener in JavaScript. To change between states, add the following script tag after the body inside index.html
:
<script>
const states = [
'awake',
'angry',
''
]
let i = 0
document.querySelector('.face').addEventListener('click', (e) => {
e.currentTarget.className = `face ${states[i]}`
if (i === states.length - 1) {
i = 0
} else {
i++
}
})
</script>
This will change the facial expression on each click. This is done by appending the appropriate class in the states array to the element on click. Using a variable (i
) for keeping track of the current index, we can start over again when we get to the end of the array. This will basically do the following:
Add the .awake
class to the element on the first click.
Add the .angry
class to the element on the second click.
Remove extra classes from the element on the third click.
Once i
reaches states.length -1
, we reset it to 0
to start from the beginning.
In summary, CSS @keyframes
can be used to generate complex animations through positioning, transformation, and opacity. If you’d like to learn more about CSS animations, check out the following roadmap, which takes you through many examples of how to use transitions to animate from one state to the other.
As mentioned in the beginning, you can find the source code in one piece for this tutorial on GitHub. Thank you for reading. Happy animating 🎨!