⚠️ Motion Warning: The Glitch page preview contains animated elements.
The CSS animation
property makes it possible to create simple UI animation with CSS only. No library to install, import, or load! In addition, most animation libraries build on, or use similar functionalities as, CSS animation; knowing the basic principles of CSS animation helps you learn these animation libraries more efficiently.
This post is a quick introduction to CSS animation to accompany this Glitch page, which I created to demonstrate simple CSS animation in the form of single blinking dots. I'm also including references if you'd like to learn more.
You can view the source code below and/or click "Remix" to try it yourself.
Write your first CSS animation
To animate an element, we need to declare the following:
1) The @keyframes
at-rule
@keyframes blink {
0% { opacity: 0; }
50% { opacity: 1; }
100% { opacity: 0; }
}
-
blink
is the animation name. You can use any name—just make sure you use the correct name in theanimation
property. - I animate the opacity from
0
to1
and back to0
. We can[⚠️] animate most CSS properties (except for ones that don't make sense likebackground-image
, that is).- If you're curious, here is MDN's list of animatable CSS properties.
- ⚠️ …But doesn’t mean we should. Animating properties other than
transform
andopacity
is taxing on the browser and may hurt performance. See Chen Hui Jing’s post for a good explanation on how browser rendering engines process styles.
We define animation frames/states either as percentage (like the above example) or from
(0%) and to
(100%). If several frames have the same styles (like 0%
and 100%
above), we can group them together as with regular CSS selectors.
Other ways to define animation frames:
/* Use from - to */
@keyframes choppyBlink {
from { opacity: 0; }
to { opacity: 1; }
}
/* Group same frames together */
@keyframes blink {
0%, 100% { opacity: 0; } /* more concise! */
50% { opacity: 1; }
}
This alone will not do anything, though. We need to assign the animation to our element.
2) The animation
property
.dot {
animation: blink 2s infinite;
}
- Apply the animation named
blink
to elements with the class.dot
.- If there is no
@keyframes blink
in our stylesheets, nothing happens.
- If there is no
- The animation-duration is
2s
. With the code samples above, the element starts withopacity: 0
, the opacity gradually increases until it reaches1
within one second, and goes back to0
within another second (total of two seconds).- Longer duration = slower animation, shorter = faster.
- The animation-iteration-count is
infinite
, so it loops forever.- If we replace it with
3
, our element animates 3 times.
- If we replace it with
- I don't declare animation-delay here. By default the delay is 0 second, ie. the animation runs directly as the element is rendered.
- If we replace it with
4s
, the animation starts running 4 seconds after the element is rendered.
- If we replace it with
Common basic variations:
/* Minimal example */
.dot--basic {
animation: blink 2s infinite;
}
/* Run animation once */
.dot--once {
animation: blink 2s 1;
}
/* Wait 4s before running the animation */
.dot--delayed {
animation: blink 2s infinite 4s;
}
Putting it together, here's the minimal CSS animation example.
@keyframes blink {
0%, 100% { opacity: 0; }
50% { opacity: 1; }
}
.dot {
animation: blink 2s infinite;
}
- Animate elements with the class name
.dot
immediately after render - Animate from zero to full and back to zero opacity within two seconds (one second each way) forever
But wait... what values exactly can we use in the animation
property? Read on!
CSS animation properties
The animation
property is a shorthand property to define multiple animation sub-properties, similar to the background
property.
So, our example above
.dot--delayed {
animation: blink 2s infinite 4s;
}
is identical to
.dot--delayed {
animation-name: blink;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-delay: 4s;
}
There are a total of eight (8) animation sub-properties:
- ⭐️ animation-name
- ⭐️ animation-duration (example:
2s
) - animation-timing-function (example:
ease
,ease-in-out
,linear
) - ⭐️ animation-delay (example:
2s
) - ⭐️ animation-iteration-count (
infinite
or any integer) - animation-direction (
normal
|reverse
|alternate
|alternate-reverse
) - animation-fill-mode (
none
|forwards
|backwards
|both
) - animation-play-state (
paused
|running
)
I only discussed the four most common sub-properties here (marked with the ⭐️ emoji), but you can see examples for all sub-properties in MDN Web Docs.
Interestingly, the order of these properties does not matter when you use them in the animation
shorthand, except for one rule: animation-duration
has to be before animation-delay
.
Browser support and vendor prefixes
You may come across CSS animation code that contains vendor prefixes such as -webkit-
, -moz-
, and so on.
With vendor prefixes, the code from the first example looks like this.
@-webkit-keyframes blink {
0%, 100% { opacity: 0; }
50% { opacity: 1; }
}
@-moz-keyframes blink {
0%, 100% { opacity: 0; }
50% { opacity: 1; }
}
@-o-keyframes blink {
0%, 100% { opacity: 0; }
50% { opacity: 1; }
}
@keyframes blink {
0%, 100% { opacity: 0; }
50% { opacity: 1; }
}
.dot {
-webkit-animation: blink 2s infinite;
-moz-animation: blink 2s infinite;
-o-animation: blink 2s infinite;
animation: blink 2s infinite;
}
At the time of writing, around 96.82% of users worldwide are on browsers that support unprefixed CSS animation, according to caniuse.com. Unsupported browsers will not get any error; they simply will not get the animation (CSS is awesome this way! 😉).
Use your own discretion based on your target users to decide whether to include vendor prefixes. (If you use any kind of preprocessor, these will be automatically added based on your configuration.)
Make sure it's accessible
1. Turn off our animation for users who prefer reduced motion
Users may specify their preference for reduced motion in their device's settings, for example because it triggers motion sickness.
If your animation is not crucial to the UI, disable it. If it's crucial (ie. conveys something meaningful), define an animation with the minimum movement possible.
@media (prefers-reduced-motion: reduce) {
.dot {
animation: none; /* or define an alternate animation */
}
}
2. Add appropriate ARIA role/attribute if necessary
If you create an extra element for animation that has no meaningful content, add aria-hidden="true"
so screen readers could skip it.
If the animated element functions as an image, for instance a pulsating font icon or element with background image, add role="img"
and aria-label
attributes, for example:
<div class="icon icon-star-bg" role="img" aria-label="Favorite"></div>
Conclusion
I'm only covering the surface here, but hopefully enough to get you familiar with CSS animation. My Glitch demo page only contains basic examples using opacity
and transform
. Remix it (or create a new one) and get creative with the animations!
Further reads
- CSS animation
- Using CSS animations - MDN Web Docs
- animation property by Chris Coyier, CSS Tricks
- caniuse.com browser support
- Reduced motion
- An Introduction to the Reduced Motion Media Query by Eric Bailey, CSS Tricks
- prefers-reduced-motion: Sometimes less movement is more by Thomas Steiner, web.dev
- ARIA roles/attributes
- Introduction to ARIA in web.dev
- img role - W3C Recommendation
- aria-hidden state - W3C Recommendation
- ARIA: img role - MDN Web Docs
- Using the aria-hidden attribute - MDN Web Docs
Top comments (3)
Nice post!
So, I did mine quite similar but slightly different using
background-color
for my use case. But, I think there's a slight tweak for the reduced motion that's worth pointing out:The
update: slow
is for devices that cannot handle the animation (smashingmagazine.com/2021/10/respe... aka targeting a screen with a low refresh rate.And the alternative to full removal of the animation is talked about here:
css-tricks.com/revisiting-prefers-...
Great post, I didn't know about the media query for reduced motion or even to consider this as an accessibility concern, good to know!
Glad it helped! I'd normally just put that media query in the global CSS with the universal selector
*
and override if needed.