paint-brush
Creating Facial Expressions with CSS Animationsby@webtips
798 reads
798 reads

Creating Facial Expressions with CSS Animations

by Ferenc AlmasiNovember 14th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Facial Expressions created through CSS animations
featured image - Creating Facial Expressions with CSS Animations
Ferenc Almasi HackerNoon profile picture

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:

The end result of this tutorial

You can find the source code for this tutorial in one piece on GitHub.


The HTML Layout

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.

DOM elements explained


Styling the Element

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.

translate helps us to center the element

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.

Position of ::before and ::after illustrated

Adding the screen

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;
}

Before and after adding the ::before element to the .screen



Adding Expressions

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.

Red bottom border against a black background

The sleep animation

We want to complement the current expression with a sleeping animation to demonstrate the power of CSS animations. We’ll add the following animation:

Sleep 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:


  • awake: The default state after waking up the monitor from its sleep. We’ll see how we can create a blinking animation.
  • angry: We’ll look into CSS rotation to see how we can make the element shake.

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).

Awake

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.

Blinking animation

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%.

Angry

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.

Shaking animation


Switching Between States with JavaScript

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.


Summary

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 🎨!