Author: Mason N. Gobat (masongobat@gmail.com) Maintained by: David J. Birnbaum (djbpitt@gmail.com) Last modified: 2021-12-27T23:34:43+0000
This tutorial will introduce a range of design-oriented principles and practices related to creating CSS animations. It is not a quick overview because the goal is to introduce CSS animation in enough detail for you to be able to apply the technology to your own project. With that said, feel free to read though this tutorial quickly at first to orient yourself in the content and then work through the different sections at your own pace.
The main parts of this tutorial are:
We illustrate our tutorial with examples from a sample project that is available, in its final form, at https://codepen.io/masongobat/pen/qBXMNMM?editors=1100; to see the animation at that site, mouse over the image. We encourage you to implement the code for our sample project yourself, one step at a time, as you read through this tutorial, since step-by-step hands-on experience with the details will help you become comfortable with methods that you will then be prepared apply in your own work.
This section will cover CSS, web development, and design principles that are prerequisites for working with CSS animation. Even if you have prior experience with these topics, we encourage you at least to review this section to refresh your memory. The topics discussed in this section are Pseudo-elements and pseudo-classes, Variables, Color schemes, and Measurement units.
::before
,
::after
,
:hover
, and
:root
Pseudo-elements allow CSS to insert specialized new elements and
content into a document. These elements are not present originally in your HTML,
but the CSS pseudo-selectors cause them to be inserted into the DOM (the version
of the HTML that the browser creates in memory), which means that they behave,
for our purposes, like regular elements. The most important pseudo-elements for
animation are ::before
and
::after
. A pseudo-element is defined with a
suffix that is appended to a regular element selector, e.g., the selector
p::before
will insert a pseudo-element (which
may have content and styling) into your document before any
<p>
element
We also use two pseudo-classes:
:root
and
:hover
.
:root
is a stand-alone selector
that refers to the root element of the document, whatever it may be. What we
mean by stand-alone
is that, unlike
::before
and
::after
,
:root
is not appended to anything; it is an
independent selector, just like a regular selector that names an element type.
If the root element of your document is
<html>
, the CSS selectors
:root
and
html
have the same meaning. The pseudo-class
:hover
is not stand-alone; it is appended to
an element selector, so that, for example,
p:hover
lets your CSS distinguish styling that
is normally applied to <p>
elements from
styling that is applied to <p>
elements
only while you are hovering over them with the mouse cursor.
Pseudo-elements begin with two colons and pseudo-classes begin with one.
Browsers are often lax about the distinction, but your CSS will be more
legible if you adopt the conventions of the CSS specifications.
Pseudo-elements create new objects in the DOM, while pseudo-classes refer to
existing objects, either in general (:root
always refers to the root <html>
element in an HTML document) or under certain conditions (a selector that
ends in the pseudo-class :hover
applies
styling to an element of that type whenever the mouse is over it).
Variables (also called CSS custom properties) make it easier to manage your CSS in ways that we illustrate below. As in mathematics, variables are names that refer to pieces of data and that can be used in place of the literal values. For example, if you apply the same specific color to many of your CSS elements and you later want to change it for all of them, you would have to edit the CSS separately everywhere you use the value. If, though, you assign the color value to a variable and use the variable name in your styling rules instead of the literal color value, you could change only the one assignment line and the new value would be used everywhere the variable was referenced in your stylesheet.
Variables can be used on the element for which they are declared and on its
descendants (where a variable can be used is called its
scope). Variables declared as properties of
:root
are comparable to what are called
global variables in other languages; they can be used anywhere in
your CSS because everything is a descendant of the root.
Let’s apply the preceding information to the project we mentioned earlier, starting with some HTML that is simple enough not to distract from our main focus, which will be on the CSS that controls the animation. Below we also supply an initial CSS document, to which we will add gradually, so that your page, if you are coding along with us, will eventually look the same as the Codepen example above. Our initial HTML is:
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Intro CSS Animation</title>
<link rel="stylesheet" type="text/css" href="style.css"/>
</head>
<body>
<h1 class="title">Intro to CSS Animations</h1>
<div class="card">
<div class="cardContent">
<h2 class="cardTitle">This is a title</h2>
<p>Lorem ipsum dolor sit amet.</p>
</div>
</div>
</body>
</html>
and our initial CSS, called style.css (to match the @href
value in the <link>
element in the
HTML), is:
body {
background: #f4eee2;
}
.title {
display: flex;
justify-content: center;
width: 100%;
}
.card {
}
Caution: Here and below, it is easy when creating CSS to
overlook the dot at the beginning of some selectors, such as
.title
and
.card
, above. The dot means to select any
element that has a @class
value that contains the word
title
, which in the case of our HTML is
<h1 class="title">
. If you forget
the dot, you won’t style the elements you want to style; what you will be
doing instead is asking to style elements with the names
<title>
and
<card>
. It’s easy to overlook the
dot, and if your CSS looks good but some of your specifications don’t seem
to be applied to your HTML, scrutinize those code blocks to verify that you
haven’t accidentally omitted the dot required for a @class
selector. (If none of your CSS is applied, it’s probably because
your HTML cannot find the CSS file, either because the filename in the
@href
attribute does not match the actual name of your CSS file
or because the CSS file is not where the HTML document is looking for
it.)
Our initial background value, above, is a hard-coded color. In a stylesheet as
brief as this one it would be easy to find and change the color, but in
anticipation of being able to work with longer, more complex stylesheets, we now
modify the CSS to add a :root
selector and
assign the color there to a variable we’ll call
--backgroundColor
:
:root {
--backgroundColor: #f4eee2;
}
Once we’ve declared a variable, we can use it in place of its literal value, so
we can change our rule for the <body>
element to:
body {
background: var(--backgroundColor);
}
A variable name must begin with two hyphens, but otherwise the name is up to you.
When we use a variable (but not when we declare it) we have to wrap its name in
var()
. In the example above our introduction
of a variable has made our CSS a bit longer and bit less direct, but part of the
benefit of the variable is already evident: if we use a value like
#f4eee2
in multiple places in our document, we
won’t easily remember that we’re using the same value that we used somewhere
else, and we won’t easily remember that it’s the same value that we used as our
background color. The variable thus helps us document the meaning of our CSS
value, which makes our code easier to read, understand, and maintain.
There are three common conventions for naming variables, all of which will get the job done, and what is most important is to choose one and apply it consistently within a project. A CSS processor won’t care about consistency, but being consistent makes it easier for you to remember your variable names. If you wind up constributing to someone else’s existing stylesheet, you should adopt the variable naming convention in place there. The most popular conventions for naming variables are:
--helloWorldVariable
--hello-world-variable
--hello_world_variable
Variable values are not just for colors; they also include measurement units (see below), plain text, and more—anything that is a valid CSS value in the context in which you use it.
Color schemes are sets of colors that we define to maintain consistent and
mindful styling both within a page and across an entire site, and variables are
well suited to this purpose. For example, in the course project site http://insults.obdurodon.org we
initialize the colors we use as global variables by specifying them within the
:root
selector of the CSS:
:root {
--textColor: #E0E0E0;
--backgroundColor: #2C2A25;
--headerColor: #8F0002;
--hoverColor: #B80003;
}
By assigning the color values to variables, we can use them easily and
consistently throughout, and we’ll understand values like
var(--textColor)
, when we see it applied more
quickly and easily than we would understand a value like
#E0E0E0
. Using variables also makes it easy to
change our color-scheme decisions, since if we decide we would prefer a
different text color, we can change just the variable declaration, and the new
color will be used everywhere the variable is referenced.
There are different ways to specify color values, which include:
red
): These are simple and
self-documenting, since the values correspond, at least in most cases, to
the way humans refer to colors. The limitations of using color names include:red
means what we think it means (although we cannot be
certain that it will give us the exact shade of red that we want),
but most developers will not know that there is no color value
puce
, but that there is a color value called
peru
, and that it is sort of an orange brown.#f4eee2
): These are six-digit
hexadecimal numbers. The first pair of digits represents the amount of red
in the color, the next pair the amount of green, and the last pair the
amount of blue. This method can address more than 16 million colors, but the
values are not easily recognizable. Developers often rely on sites like https://htmlcolorcodes.com/ to
mix colors and generate a hex value that can be copied and pasted into a CSS
stylesheet.rgb(244, 238, 226)
): The name
stands for red, green, blue, and the information is the same as with
hexadecimal notation, except that it is expressed as three decimal values
(e.g, hex f4
= decimal 244
). An optional fourth
value, a decimal value between 0 and 1, can be specified with
rgba()
and controls the opacity, which
ranges from 0 (transparent, which is invisible) and 1 (fully opaque, which
is the default).hsl(40, 45%, 92.2%)
): The name
stands for hue, saturation, lightness. Hue is a value from 0 to 360 (0 = red, 120 = green, 240 = blue), saturation (amount of gray mixed in) is a percentage in which a higher value means less gray, and lightness (also a percentage) is the balance of black (0%) and white (100%). As with RGB, an optional fourth value, a percentage or a decimal value between 0 and 1, can be specified with
hsla()
and controls the
opacity. HSV (hue, saturation, value) is similar to HSE; for more
information see HSL and
HSV on Wikipedia.We recommend using a consistent color system for identifying color values within
your site. If you use any method other than HTML color names, we also recommend
including CSS comments when you declare your color variables, which will help
you remember that, for example, our #f4eee2
example is a sort of beige.
There are two main types of measurement units:
Static units are less useful than dynamic ones because their non-contextual
meaning poses challenges to harmonizing measurements within a page. For example,
an element with a left: 100px;
will not change
its distance from the left margin when the user adjusts the width of the window,
but a property value like left: 10%;
will
always be 10% of the way from the left edge of the window to the right, and the
exact distance will therefore be proportional to the window width. The dynamic
units we use most are:
font-size:
property), the actual size of
an em changes accordingly.:root
,
whether specified or defaulted, and not the font in use for the current
element. The size of a rem (unlike an em) is thus invariant within a
document, which makes it useful for maintaining a consistent overall
appearance in contexts that mix fonts or font sizes.Now we're going to get into the real meat of this tutorial, so pull up your favorite HTML text editor, a cup of hot cocoa (tea or coffee are also acceptable), and prepare to astound your friends and relatives with your masterful CSS animations!
The first and most important advice that we can offer about creating animations in CSS is to start at the end. What we mean is that when creating an animation you will find it easier to build what you need if you first decide on the appearance you want to have achieved after the animation has finished. For that reason we first add all the content and styling that we want to display once the animation is finished, and only then do we add the code that controls the transitions that cause those features to emerge gradually.
At the top of the HTML page we introduced earlier we
have an <h1>
header element that acts as
a title and is centered by our CSS. Once we move down into our outer
<div>
, the @class
values on
that element and its descendants will allow our CSS to control the animation
associated with it. Our
<div class="card">
contains all the
content that we will display in a single card, which is the term we
use here to refer to an image with an associated title
(<div class="cardTitle">
) and other
content. In a real project we might have multiple cards, each with its own image
and text, but for this exercise we create only one.
We already created a placeholder .card
selector
in our skeletal CSS, to which we now add some basic styling:
.card {
color: lightgray;
width: 25%;
padding: 18rem 0 0;
}
When you reload your HTML in a browser you will notice two things. First, the
text within the card is going to be light gray because that’s the value we
specified for the color:
property, which
controls the text color (this will become more legible later when we specify a
darker background behind it). Second, the location of your card content has
moved within the screen. To see why this is happening add the following line to
the .card
section of your CSS:
outline: 1px solid black;
You should see an outline around your card, which shows that the text is inside
the card, at the bottom edge. The padding that we added creates space above your
content, so the height of the card is determined by the height of its content
(two elements, an <h2>
and a
<p>
) plus 18 rem units of padding at the
top. You will see why this is useful in the next step.
Pause now to experiment with the HTML and CSS. If you add more lines of
content to the HTML, the box extends downward (if you remove content, it
shrinks upward), so that it always wraps however much content the
<div>
contains. The width is always
25% of the screen width; if you stretch or narrow the screen, the box adapts
accordingly. If you change the padding value, the box adapts similarly to
the new value. When three values are supplied for the
padding:
property, as is the case in this
example, the first value applies to the top, the second to the left and
right, and the third to the bottom.
You have probably already used the <img>
element to display an image, but did you know that you could also use CSS to add
images to your page? To specify a background image for an element using CSS you
set the background-image:
property for that
element to point to the image file, which can be a local file or one accessed
with a URL over the Internet. The image specifier (even if it is a local file)
must be wrapped in url()
. In this example we
set an image of a raccoon as the background of our card with the following
code:
.card {
color: lightgrey;
width: 25%;
padding: 18rem 0 0;
background-image: url(https://i.natgeofe.com/k/40653a53-9fa6-44da-bc90-c4c674616a04/raccoon-road_2x3.jpg);
}
This CSS is loading the image, but it may look as if all that has happened is that it has set a plain gray background. That effect arises because the original image size is very large, and unless we specify otherwise, only a corner of it, which happens to be gray and happens not to contain any of the raccoon, fits in the available space, so that’s all that is rendered. To fix this add one more CSS property:
background-size: cover;
This property will stretch or crop the image to fit the available size. The text is still hard to read against the background, but we’re about to fix that.
There are two final pieces that we are going to add to our CSS before we begin animating. These pieces are optional, but the first is helpful for text readability and the second is just a nice touch. First, we are going to add a gradient (gradual shading) behind all the text that we are displaying on the card, which will let us improve the legibility by making the background darker toward the bottom of the card, where the light gray text is rendered. We are going to do this using hsla, but you could also do it using rgba. We encourage you to experiment with different numerical values to get a sense of how the numbers are translated into specific rendering effects. The a in hsla stands for alpha, which is the technical term for the property that controls the opacity of an element (such as an image) during rendering. As we mention above, the alpha value can be expressed as either a percentage or a decimal value between 0 and 1. Now add the following code to your CSS and reload:
.cardContent {
--padding: 1.5em;
padding: var(--padding);
background: linear-gradient(
hsla(0, 0%, 0%, 0),
hsla(0, 0%, 0%, .3) 15%,
hsla(0, 0%, 0%, 1)
);
}
The preceding code declares some CSS properties for the
<div class="cardContent">
element. We
add some padding on all sides (which expands the size of the card, and therefore
of the outline box we drew earlier), and we also add a
linear-gradient()
value to the
background:
property. The
background:
property is an omnibus way of
specifying several aspects of background rendering that can also be specified
individually, including color, image, position, size, etc. We’ve already
specified the background image and size, so our
background:
property here just adds the
gradient to the values already in effect.
The linear-gradient()
property can be applied
to a variety of CSS selectors, each offering a cool new effect. In this case we
supply three hsla values for linear gradient, which means that
we will have a transition among three different values. The minimum number of
values is one, but then we wouldn’t get a real gradient, and there’s no upper
limit to the number of values you can include. The order of appearance in the
list controls how the gradient will be rendered (from top to bottom by default;
it is possible to specify other directions), and the percentage after the second
value is where, from top to bottom, to begin to implement the transition to the
next value. In this case we gradually shade the opacity value from 0
(transparent) to .3 over the first 15% of of the
<div class="cardContent">
element (not
the entire card, just the box at the bottom that contains the text) and then
from .3 to 1 (opaque) over the remaining 85%. The gradient thus darkens just
where we need it to so that the light gray text shows up against it.
Our card has an animated underline that grows, when we mouse over the card, from
the left edge of the card to the end of the title. This is not a normal
underline, of the sort that can be specified with the CSS
text-decoration-line:
property, because that
property provides limited flexibility with respect to animation. What we do
instead is use a CSS pseudo-element to specify a 4-pixel-high background
that is the same color as the text, effectively creating a thin underline. The
additional CSS, before we introduce the animated transition, is:
.cardTitle {
position: relative;
width: max-content;
}
.cardTitle::after {
content: "";
position: absolute;
left: calc(var(--padding) * -1);
bottom: -5px;
height: 4px;
width: calc(100% + var(--padding));
background: lightgrey;
}
Here we create a .cardTitle::after
pseudo-element, which allows our CSS to insert something that behaves like an
HTML element, and that therefore has content and can be styled. The
content:
property is required, and because we
don’t actually want to insert any new textual characters, we leave it as an
empty string. The reason for thus subterfuge is that it lets us insert just our
fake underline, with no actual additional text, in a way that is sensitive to
the size of the title we are underlining. In order to position our line
correctly with respect to the title, we must set the
position:
property of the title to
relative
and the
position:
of the pseudo-element to
absolute
. The
width:
property for the title makes the
underline stop at the right edge of the content; without it the width would be
computed as equal to that of the wrapper
<div>
because header elements default to
the width of their containers, rather than their text.
If you remove the width:
specification for
<h2 class="cardTitle">
the width
will default to width of its container, which is wider than the length of
the title.
Setting the position:
value of the title to
relative
and that of the pseudo-element to
absolute
makes it possible to position the
underline relative to to the title element. By then specifying
bottom: -5px;
we render the pseudo-element
5 pixels below the bottom of the title, so that it looks like an underline.
Absolute positioning for the pseudo-element works only if the positioning of
the regular element is set to
relative
.
<oXygen/> will report an error on
left: calc(var(--padding)* -1);
because
<oXygen/> does not have very robust CSS validation. The W3C Jigsaw CSS
validator at https://jigsaw.w3.org/css-validator/ correctly recognizes this
input as valid CSS.
Here we have introduced the CSS calc()
function, which can compute new numerical values based on input that consists of
regular numbers, variables, and measurements. We want to draw a line that spans
from the left edge of the <div>
element
that contains the title (including its padding, the space between the edges of
the element and whatever it contains) to the end of our title text, and because
the length of the title text is not predictable in advance and will be different
for different titles, we use calc()
to compute
the values we need to position the line. Our two
calc()
statements work as follows:
left: calc(var(--padding) * -1);
establishes the left edge of the underline. The
--padding
variable, which was set in the
CSS rules for the parent
<div class="cardContent">
element, lets
us back up the left edge of the line so that we underline from the left edge
of the image, which is 1.5em to the left of where the title text (the
<h2>
element) begins.width: calc(100% + var(--padding));
sets
the width of the underline equal to 100% of the width of the text inside
<h2>
title element (this is why we
specified width: max-content;
for the
title) plus the width of the left padding (which is easy to access and reuse
here because we assigned the value to a variable). Combined with the
preceding CSS left:
value, this draws the
underline from the left edge of the container
<div>
to the right edge of the title
text.Now that we've completed what we want our card to look like in its final state, we can begin to animate the pieces so that they work together to create the product that we want. The next section will cover what we like to call the two Ts of animation: transform and transition. Transform can adjust the size or position of an element, and more, but by itself it doesn’t produce a smooth animation because everything will just snap into place. Once we add the transition, though, changes in styling will happen gradually in accord with our specification.
The transform:
property has two functions that
are helpful for moving elements around:
translate()
and
scale()
. We use
translate()
anytime that we want to move
something, such as sliding the title underline in from the left. We use
scale()
in order to make the size of the card
increase on hover. Hover is the CSS term for mouseover, implemented
with the :hover
pseudo-class, which we
illustrate below.
Scaling is the simpler transformation, so first add the following code to your CSS file:
.card:hover {
transform: scale(1.05);
}
The :hover
pseudo-selector allows you to
specify behaviors for the element on hover. If you reload the file and mouse
over the image, you’ll see it quickly snap to a larger size (we’ll make the
transition gradual shortly). Now add the line
transform: translateY(50%);
to the CSS for
your .cardContent
class, so that it looks like
the following:
.cardContent {
--padding: 1.5em;
padding: var(--padding);
background: linear-gradient(
hsla(0, 0%, 0%, 0),
hsla(0, 0%, 0%, 0.3) 15%,
hsla(0, 0%, 0%, 1)
);
transform: translateY(50%);
}
Your card now looks longer because 50% of the height of the
<cardContent>
element has been shifted
below it by the translate()
function. We make
this extra length disappear (temporarily) by adding
overflow-y: hidden;
to the parent
.card
selector, so that our CSS specification
now looks like:
.card {
color: lightgray;
width: 25%;
padding: 18rem 0 0;
outline: 1px solid black;
background-image: url(https://i.natgeofe.com/k/40653a53-9fa6-44da-bc90-c4c674616a04/raccoon-road_2x3.jpg);
background-size: cover;
overflow-y: hidden;
}
Once you do this, the part of the content that was rendered below the card is
hidden. We still need to add two more specifications to our CSS to implement the
animation. First add two lines to your CSS rules for
.cardTitle::after
:
transform: scaleX(0);
transform-origin: left;
The whole block for that selector now looks like the following:
.cardTitle::after {
content: "";
position: absolute;
left: calc(var(--padding) * -1);
bottom: -5px;
height: 4px;
width: calc(100% + var(--padding));
background: lightgrey;
transform: scaleX(0);
transform-origin: left;
}
Now create a selector that will apply to the card title underline when we mouse over the card that contains it (and not only over the title underline itself—which is, after all, invisible until we render it):
.card:hover .cardTitle::after {
transform: scaleX(1);
}
We’ve now specified an initial scaling value of 0 for the card title underline,
which changes to 1 when we mouse over its parent card. Valid values for
scale()
range from 0 to 1, where 0 makes
something invisible (0% of it is visible) and 1 makes it entirely visible
(100%). We specified transform-origin: left;
to ensure that our line will grow from the left border of the card, rather than
spreading out from its center, which is the default.
We also want the text that she shifted below the image and hid to slide up into place on hover, and we do that by adding the following to our CSS:
.card:hover .cardContent {
transform: translateY(0);
}
This says that when we hover over the card we will undo the downward shift of its card content descendant, which will cause it to move back up into visible space within the card.
We are now at our final stage: on hover our image snaps to a larger size, the
underline appears under the title, and the text snaps up into visible space over
the card iamge. We need to modify those transitions to make them gradual, which
will be more pleasing to the eye than the current harsh jumps. We implement
these smooth transitions by adding the line
transition: 750ms linear;
to the regular (not
hover) selectors for every element that will change on hover, that is, every
element that has a corresponding hover specification that contains a
transform:
property. In our example those
elements are the selectors for .card
(the
scale changes on hover), .cardContent
(the
content shifts back into visible space on hover), and
.cardTitle::after
(the underline is drawn on
hover). The first of the values that we give for the
transition:
property specifies the amount of
time we want to elapse between the beginning state to the end state of our
animation. The second value is how we want the transition to look, where a
linear transition is implemented as a regular, steady-state progression from the
initial state to the final one. We encourage you to experiment with some of the
other options, and the values we use most include:
There are many online resources that will help you to decode what each of these
does if the behavior isn’t apparent while you are experimenting, and you can
temporarily increase the transition time to slow the transition, which may make
the way the appearance changes easier to see. It is also possible to add a value
to the transition:
property to delay the onset
of the transition, which is useful if you want one animation to finish before
another with the same trigger begins. For now, though, pat yourself on the back
and reflect on the beauty of the animations you have just implemented, and on
having acquired the knowledge and tools to create your own animations in the
future.
Our final, complete CSS looks as follows:
:root {
--backgroundColor: #f4eee2;
}
body {
background: var(--backgroundColor);
}
.title {
display: flex;
justify-content: center;
width: 100%;
}
.card {
color: lightgray;
width: 25%;
padding: 18rem 0 0;
outline: 1px solid black;
background-image: url(https://i.natgeofe.com/k/40653a53-9fa6-44da-bc90-c4c674616a04/raccoon-road_2x3.jpg);
background-size: cover;
overflow-y: hidden;
transition: 750ms linear;
}
.cardContent {
--padding: 1.5em;
padding: var(--padding);
background: linear-gradient(
hsla(0, 0%, 0%, 0),
hsla(0, 0%, 0%, .3) 15%,
hsla(0, 0%, 0%, 1)
);
transform: translateY(50%);
transition: 750ms linear;
}
.cardTitle {
position: relative;
width: max-content;
}
.cardTitle::after {
content: "";
position: absolute;
left: calc(var(--padding)* -1);
bottom: -5px;
height: 4px;
width: calc(100% + var(--padding));
background: lightgrey;
transform: scaleX(0);
transform-origin: left;
transition: 750ms linear;
}
.card:hover {
transform: scale(1.05);
}
.card:hover .cardTitle::after {
transform: scaleX(1);
}
.card:hover .cardContent {
transform: translateY(0);
}