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 to create animations and use different to get the desired result. At the end of this tutorial, we’ll end up with the following: @keyframes CSS properties 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. : The entire monitor that encapsulates the screen and the facial expression. We’ll use and elements to add extra visual elements. .face ::before ::after . : The screen where we’ll add the expression. screen : The expression element will hold the eyes and mouth for the animation. We’ll use and elements to create three different elements visually out of one DOM node. .expression ::before ::after Styling the Element Now that we have the layout in place, let’s take a look at the CSS. To style the element, add the following rules to the CSS file: .face 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 positioning combined with and set to . Note that we offset the element by on both the x and y-axis to center align the anchor. absolute top left 50% -50% To align the screen to the center inside the monitor, we also need to set the to with these two alignments set to the center: display flex : Set the vertical alignment. align-items : Set the horizontal alignment. justify-content To add the back of the monitor, we’ll use the and element of the element. Extend the CSS with the following rules: ::before ::after .face .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 and elements on the right side of the monitor. For the correct position, we need to take the width of the element plus the borders from each side: ::before ::after right 50px (width) + 2 * 5px (borders) -> 60px We can follow the same calculation for the element, but this time, we have to take the element into account, so we have to adjust by another . This is because we want the right border of the element to overlap with the left border of the element. Otherwise, we would end up with a border between the two elements. ::after ::before 60px 60px - 5px ::before ::after 10px Adding the screen Now that we have the base element styled, let’s also add the . For this, all we have to do is set a and along with some background and border colors. Append the following to the CSS: .screen width height .screen { position: relative; width: 220px; height: 220px; background: #4D3A29; border: 5px solid #2B1607; overflow: hidden; } Make sure you set to , as we’ll have cases where we animate elements outside the screen. overflow hidden The element will be automatically aligned to the middle. This is because we used for the element with and set to . We also need the inner edge of the monitor, which we can achieve with another element: flex .face align-items flex-direction center ::before .screen::before { content: ''; width: 10px; height: 100%; display: block; background: #CAA277; border-right: 5px solid #2B1607; } 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 circle at from the top of the screen, horizontally in the middle (using and ) with a white background. By setting to , 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 block: 30px 75% left: 50% translateX(-50%) border-radius 100% .expression .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 to the left from the mouth, and one to the right. The crescent is achieved through the property. Here, we use a as well, and by using a single border, we can essentially generate a crescent. 30px 30px border-bottom 100% border-radius As we want to animate these elements, make sure you define a rule. transition The sleep animation We want to complement the current expression with a sleeping animation to demonstrate the power of . We’ll add the following animation: CSS animations Let’s create the element first, and then we create the animation for it. For the element, we’ll use the of our element. Add the following to your CSS to introduce the “Z“: ::after .screen .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 CSS selector. For additional states, we’ll simply use additional classes. We’ll have the following states: :not : The default state after waking up the monitor from its sleep. We’ll see how we can create a blinking animation. awake : We’ll look into CSS rotation to see how we can make the element shake. angry We have this element absolutely positioned at the top-right part of the screen, with some rotating applied. It calls the 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. sleep : The time it’ll take for our animation to play. We’ll complete the animation in four seconds. 4s : 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 . ease-in-out easings.net : The number of times we want to place this animation. To play it indefinitely, we can use . infinite infinite To create an animation in CSS, we need to use the keyword with the name of the animation: @keyframes @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. : Initially, we’ll start with a 0% and slowly fade in the element over 30% of the animation. 0% opacity : At 30%, the element is fully faded in, and we also shift it to the right by . We can also change the content. 30% 20px (50px - 30px) : At 60%, we change the content again and pull the element back to its initial position ( from the right). 60% 50px : We reiterate the same steps for the right position to pull and push the element back and forth, creating a waving animation. 90% 100%: We want the element to move upwards, outside of the screen. We can achieve this by animating the property to a negative value. Note that there are no stops in between for this property. We go from the initial position ( ) to its final position in one step ( ). top top: 45px 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. 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 . 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% applied for the element). transparent border-radius For the blinking animation, we’ll need to create a new . This time, it’s using a custom for the easing, which creates a more natural transition. Now, let’s take a look at the animation: @keyframes cubic-bezier blink @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 animation is simply an iteration of scaling the eyes back and forth from to . 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%. blink 1 .1 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 easing. Let’s take a look at what the shake animation looks like: linear @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 property set to on both the x and y-axis, we need to pass it alongside the rule to prevent the element from losing the rule. All that happens here is that the element is rotated back and forth between and , creating the shaking effect. By increasing or decreasing the degree amount, we can make the effect more subtle or amplify it. translate -50% rotate translate -15° 15° Don’t forget to always return to the initial state at 100% to create a smooth loop. 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 ( ) 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: i Add the class to the element on the first click. .awake Add the class to the element on the second click. .angry Remove extra classes from the element on the third click. Once reaches , we reset it to to start from the beginning. i states.length -1 0 Summary In summary, CSS 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. @keyframes https://webtips.dev/roadmaps/css-animations?embedable=true As mentioned in the beginning, you can find the in one piece for this tutorial on . Thank you for reading. Happy animating 🎨! source code GitHub