Digital humanities


Author: Mason N. Gobat (masongobat@gmail.com) Maintained by: David J. Birnbaum (djbpitt@gmail.com) [Creative Commons BY-NC-SA 3.0 Unported License] Last modified: 2021-12-27T23:34:43+0000


A comprehensive and design-oriented introduction to CSS animations

Foreword

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:

  1. CSS design principles and fundamentals
  2. How to implement CSS animation
  3. For more information

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.

Important CSS design principles and fundamentals

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.

Pseudo-elements and pseudo-classes: ::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

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:

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

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:

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.

Measurement units

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:

Animation development

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!

Start at the end

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:

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.

Transform

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

For more information