While theĀ transformĀ property has your back for most of your animation needs, there is a glaring hole in its coverage: color.
Changing the color of elements is an essential part of animating, but theĀ transformĀ property leavesĀ you high and dry. And the only other choice you have, if you want to ensure that you have smooth animation is theĀ opacityĀ property.
So⦠are color animations off the table then?
No.
No, no, no.
You can totally create high-performance color animations, but you have to approach your code a little differently than weāve been doing.
Up until now, weāve written our HTML structurally, creating elements as part of the layout.
You can also approach writing your markup with functionality in mind, where you create elements, not for the layout, but rather to serve as pieces of your animations themselves.
In this chapter, weāre going to look at how you can organize your code so you can perform color animations using only the opacity property.
Letās say that you have a button that changes color when it is hovered over:

The change in color provides the user feedback that it is an element that they can interact with, which helps improve the userās experience on your site. But rather than an instantaneous change between colors, adding a brief transition to blend between them could help theĀ :hoverĀ state feel smoother and more natural.
Taking a look at the code, you can see that the button is a Ā <button>Ā element with theĀ .btnĀ class assigned to it:
<button class="btn">Hover over me!</button>AndĀ .btnĀ has a Ā background-color ofĀ #15DEA5Ā via the Ā $clr-btnĀ variable, and a:hoverĀ state that darkens theĀ $clr-btnĀ Ā by 5% using Sassā Ā darken()Ā function:
$border-rad: 2rem;
$clr-btn: #15DEA5;
.btn {
border-radius: $border-rad;
background-color: $clr-btn;
&:hover {
background-color: darken($clr-btn, 5);
}
}We want to animate theĀ background-colorĀ of our button, so let's add a transition toĀ .btnĀ with a duration of 250 milliseconds:
$border-rad: 2rem;
$clr-btn: #15DEA5;
.btn {
border-radius: $border-rad;
background-color: $clr-btn;
transition: background-color 250ms;
&:hover {
background-color: darken($clr-btn, 5);
}
}Now the buttonās color blends to a darker shade when hovered over:
Perfect! Job done. Moving on. Exceptā¦
Repaints!!! š±
Animating the background-color property triggers a new paint calculation of the button for each frame of the transition. Granted, this is a basic scenario, and the performance is still acceptable. However, if this were a more complicated page and animation, jank would creep in.
So, animatingĀ background-colorĀ isnāt the best practice for color-changing animations. Youāre supposed to use theĀ opacityĀ property for that.
But theĀ opacityĀ property changes the transparency of an element and its children on a scale from 0 to 1, where a value of 0 will make the element completely transparent, and a value of 1 will be completely solid.
We want a darker button, not a transparent one. How are we supposed to change the color by changing its opacity?
Remember earlier when I talked about structuring your HTML to serve an elementās functionality, rather than just layout? What if we structured our button so that there are two separate backgrounds stacked on top of one another?
The bottom layer would have the normal, inactive color, and the top layer would have the darker color of theĀ :hoverĀ state. Then we could fade the top layer on and off with theĀ opacityĀ property, creating an animation between the two colors.
Let'sĀ add a<div>Ā after the button's text with the darkerĀ background-colorĀ and itsĀ opacityĀ set to 0, and theĀ opacityĀ set to 1 in the buttonāsĀ :hoverĀ state. So the HTML would look something like this:
<button class="btn">
Hover over me!
<div class="btn__bg"></div>
</button>And the CSS like this:
$border-rad: 2rem;
$clr-btn: #15DEA5;
.btn {
border-radius: $border-rad;
background-color: $clr-btn;
position: relative;
z-index: 1;
&:hover {
& .btn__bg {
opacity: 1;
}
}
&__bg {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: darken($clr-btn, 5);
opacity: 0;
z-index: -1;
transition: opacity 250ms;
}
}Weāve created aĀ .btn__bgĀ selector withĀ absoluteĀ Ā position and theĀ background-colorĀ set to a darkened version ofĀ $clr-btn. Weāve also added aĀ Ā z-indexĀ of -1, as well as aĀ z-indexĀ of 1 to Ā .btnĀ to create a new stacking order, where the buttonās text sits on top of our new background element.
And when we take it for a spin, we see that it works!

It appears identical to animating the background color while being faster to render. Look Ma! No repaints!

OK. NowĀ the job is done. So...moving on?
Well, we could. We have a button that changes color by taking advantage of the high-performance opacity property, which is what we set out to do. Ā But we can do better.
Right now, each added button also means adding the internalĀ <div>Ā for the background with the Ā .btn__bgĀ class assigned to it. And thatās a bunch of extra, tedious markup to write for each instance of a button, or, worse, a bunch of opportunities to make mistakes.
Rather than relying on manual labor to insert the background div into buttons, you can harness the power of CSS to magically create the background elements through the splendor of pseudo-elements! Or more specifically, theĀ ::afterĀ pseudo-element.
Of course! So...umā¦what are pseudo-elements, exactly?
Well, letās take look at the name: used as a prefix, pseudo means that something appears to be one thing, but in reality, is something else. So, a pseudo-element looks like an element, such as a Ā <div>Ā that was hand-coded into the HTML, but in fact, was generated by CSS and rendered as part of the web page. And, since a pseudo-element is still an element, you can style them in the same manner.
Adding theĀ ::beforeĀ orĀ ::afterĀ pseudo-element creates a child element wherever its selector has been assigned on a website. The element created byĀ ::beforeĀ will be the first child of the element, and those created usingĀ ::afterĀ will be the elementās last child. In the case of our button, the background element comes after the text content, so theĀ ::afterĀ pseudo-element would be the perfect replacement for our background Ā <div>.
Creating a pseudo-element is just like creating pseudo-selector, whereĀ you append the pseudo-element to a selector, but rather than using Ā a single colon ( : ), pseudo-elements use a pair of colons ( :: ) as a prefix:
$border-rad: 2rem;
$clr-btn: #15DEA5;
.btn {
border-radius: $border-rad;
background-color: $clr-btn;
position: relative;
z-index: 1;
&:hover {
& .btn__bg {
opacity: 1;
}
}
&::after {
//style the ::after pseudo selector here
}
&__bg {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: darken($clr-btn, 5);
opacity: 0;
z-index: -1;
transition: opacity 250ms;
}
}And, just like any other CSS element,Ā you need to give it some styling. We wantĀ ::afterĀ to have the same appearance asĀ .btn__bg, so letās moveĀ its styling fromĀ .btn__bgĀ to theĀ ::afterĀ pseudo-element, and then delete theĀ .btn__bgĀ selector, since we won't be needing it anymore:
$border-rad: 2rem;
$clr-btn: #15DEA5;
.btn {
border-radius: $border-rad;
background-color: $clr-btn;
position: relative;
z-index: 1;
&:hover {
& .btn__bg {
opacity: 1;
}
}
&::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: darken($clr-btn, 5);
opacity: 0;
z-index: -1;
transition: opacity 250ms;
}
}Now we have a styledĀ ::afterĀ pseudo-element, but hovering over the button won't yet produce any sort of color change because the button'sĀ :hoverĀ pseudo-selector is still selectingĀ .btn__bgĀ and changing its Ā opacityĀ to Ā 1. Instead, let's update it to use theĀ ::afterĀ pseudo-element instead:
$border-rad: 2rem;
$clr-btn: #15DEA5;
.btn {
border-radius: $border-rad;
background-color: $clr-btn;
position: relative;
z-index: 1;
&:hover {
&::after {
opacity: 1;
}
}
&::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: darken($clr-btn, 5);
opacity: 0;
z-index: -1;
transition: opacity 250ms;
}
}Our button won't be needing the backgroundĀ <div>Ā anymore, so let's remove it from our markup while we're at it:
<button class="btn">
Hover over me!
</button>And now when we refresh the page and interact with our button, it should act just like it did before:

And thatās a big fat no. Thereās no color change at all! Letās open DevTools and inspect our button:

We should be seeing anĀ ::afterĀ element where our backgroundĀ <div>Ā had been, but thereās nothing there. So, what gives?
Forgive me father, for I have sinned. I have told a half-truth. Remember when I said:
since a pseudo-element is still and element, we can style them in the same manner
That wasn't entirely true...
In my defense, that statement was 99.9% true, but the devil is in the details. Pseudo-elements require this one little property that normal ones do not. With a normal element, you code its content when you write the markup. However, since Ā ::afterĀ is effectively injecting an element into the markup after the fact, CSS needs to tell the browser what that element contains. Enter the Ā contentĀ property, which pseudo-elements require to function properly.
YouĀ can use theĀ contentĀ property to fill a pseudo-element with things like text or images, but in the case of our button, we donāt want it to have any contents. Instead, we wantĀ ::afterĀ to act as a color canvas. We also need to give content some sort of value, so letās assign it an empty string by using a pair of empty quotes:
$border-rad: 2rem;
$clr-btn: #15DEA5;
.btn {
border-radius: $border-rad;
background-color: $clr-btn;
position: relative;
z-index: 1;
&:hover {
&::after {
opacity: 1;
}
}
&::after {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: darken($clr-btn, 5);
opacity: 0;
z-index: -1;
transition: opacity 250ms;
}
}And letās take our button for a spin again and see how itās behaving:
Thatās more like it! If you take a look at things in DevTools, you'll see that thereās anĀ ::afterĀ element where our backgroundĀ <div>Ā had been:

Animating with theĀ opacityĀ property isnāt strictly limited to color changes, by the way. You can do things like animating gradients, rather than solid colors:

The only thing that has changed is that you are using a gradient for the background of the Ā ::afterĀ pseudo-element, rather than a solid color:
$border-rad: 2rem;
$clr-btn: #15DEA5;
.btn {
border-radius: $border-rad;
background-color: $clr-btn;
position: relative;
z-index: 1;
&:hover {
&::after {
opacity: 1;
}
}
&::after {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: radial-gradient(circle, lighten($clr-btn, 5) 0%, darken($clr-btn, 10) 100%);
opacity: 0;
z-index: -1;
transition: opacity 250ms;
}
}YouĀ can also create color overlays for images:

$border-rad: 2rem;
$clr-primary: #15DEA5;
@mixin peudo-pos {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.btn {
border-radius: $border-rad;
background-color: $clr-primary;
position: relative;
z-index: 1;
&:hover {
&::after {
opacity: 1;
}
& + .img {
&::before {
opacity: 0;
}
}
}
&::after {
@include pseudo-elem;
background: radial-gradient(circle, lighten($clr-primary, 5) 0%, darken($clr-primary, 10) 100%);
opacity: 0;
z-index: -1;
transition: opacity 250ms;
}
}
.img {
z-index: -1;
&::before {
@include pseudo-elem;
border-radius: $border-rad;
background: $clr-primary;
z-index: 1;
}
}The bottom line is that the opacity property will let you transition the appearance of elements without needing the browser to calculate repaints, helping to ensure that your animations have a high frame rate. And pseudo-elements save you from the tedium of writing repetitive HTML, which makes them all kinds of awesome.
And that brings us to the end of Part 2! Next weāll dive into Part 3, where we will take everything youāve learned and dial it up a notch with keyframes. So buckle up, this is going to be good!
Animating the color value of a property will trigger repaints.
To avoid repaints, use the opacity property to transition between colors, producing the same result, without the repaints.
The opacity property receives a value between 0 and 1, where 0 is completely transparent and 1 is completely solid.
To avoid hard-coding the elements to perform color changes via the opacity property, use the Ā ::afterĀ pseudo-element.
To create a pseudo-element, append the name of the pseudo-element to a selector, using two colons as a prefix: .selector::after{...}
TheĀ ::beforeĀ andĀ ::afterĀ pseudo-elements create an element that is the first or last child of the selected element, respectively.