Dec 21

CSS Animation

Review 4 types of CSS animation, from the best supported options to the most cutting-edge.

By Stephanie Eckles

Let's get right to it! We'll review:

Transitions and Transforms

Animating by transitioning transforms is the most performant way to apply subtle movement. It's often used to transition between states such as for an interactive element's hover or focus state. Transforms can also be used to provide "entrance" and "exit" animations.

Here's a selection of 4 transition utility classes using transforms that come from one of my other projects, SmolCSS. We're triggering the transition of the child elements on :hover of the parent. The reason for this is that for transitions that move the element, it could end up moving out from under the mouse and causing a flicker between states. The rise transition is particularly in danger of that behavior.

We're also wrapping our effect class in a media query check for prefers-reduced-motion: reduce that instantly jumps the transition to the final state. You can learn more about why this is important on Day 7 about preference queries.

Transition utility classes
.smol-transitions {
display: grid;
place-content: center;
}

.smol-transitions > * {
--transition-property: transform;
--transition-duration: 180ms;

transition: var(--transition-property) var(--transition-duration) ease-in-out;
background-color: lightblue;
padding: 0.25em;
font-size: 1.35rem;
border-radius: 0.25rem;
}

.rise:hover > * {
transform: translateY(-25%);
}

.rotate:hover > * {
transform: rotate(15deg);
}

.zoom:hover > * {
transform: scale(1.3);
}

.fade > * {
--transition-property: opacity;
--transition-duration: 500ms;
}

.fade:hover > * {
opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
.smol-transitions > * {
--transition-duration: 0.01ms;
}
}

/* demo dispay */
.smol-flexbox-grid {
display: flex;
flex-wrap: wrap;
gap: 1rem;
list-style: none;
padding: 0;
margin: 0;
}

.smol-flexbox-grid > * {
flex: 1 1 20ch;
margin: 0;
}

.smol-transitions {
padding: 1rem;
border: 2px dashed;
border-radius: 0.5rem;
}
  • rise
  • rotate
  • zoom
  • fade

Keyframe Animations

The next step up from transitions and transforms is keyframe animations

Keyframes are defined as different steps from 0 to 100% which correspond to that point along the duration of the animation. For example for a 400ms animation, 25% would correspond with the 100ms point.

Typically an animation moves smoothly (aka "tweens") between the keyframes, but you can also use the step() timing function to move sharply between them. This is desirable for something like sprite animation for characters to create a walking sequence.

This demo shows an example of a default tweened animation and a stepped animation. We're using the animation shorthand, and you can review all the animation options on MDN.

Demo of tweened vs. stepped animation timing
.keyframe-animations {
display: grid;
gap: 1rem;
}

.animated {
position: relative;
left: 0;
width: 12ch;
padding: 0.15em;
text-align: center;
background-color: lightblue;
animation: glide 5000ms var(--timing-function, linear) infinite;
animation-play-state: var(--play-state, paused);
}

.stepped {
--timing-function: steps(4);

background-color: goldenrod;
}

@keyframes glide {
50% {
background-color: palevioletred;
}
100% {
left: calc(100% - 12ch);
background-color: unset;
}
}
document.addEventListener("click", (e) => {
const animation = e.target.dataset.animate;

if (animation) {
const playpause = e.target;
const isPlaying = playpause.dataset.state === "playing";
const animate = document.querySelector(animation);

if (isPlaying) {
animate.style.setProperty('--play-state', 'paused');
playpause.dataset.state = 'paused';
playpause.textContent = "Play animation";
} else {
animate.style.setProperty('--play-state', 'running');
playpause.dataset.state = 'playing';
playpause.textContent = "Pause animation";
}
}
});
I'm tweened
I'm stepped

Learn more about how to think about and create keyframe animations from Amit Sheen at LondonCSS.

Motion animation with offset-path

CSS motion path allows moving an object along a path which is drawing using SVG path syntax. Previously called motion-path, the current critical property is now offset-path. It is at least partially supported in Chromium and Firefox with Safari support on the way (currently in the technical preview).

While there are times this can remove the need for a heavier JS library solution, the biggest drawback is lack of built-in native support for scaling the path or making it relative to a container. Depending on your viewport width, you may or may not see the entire timeline of the demo. Michelle Barker has a resizing solution via scaling, and Jhey provides a tutorial as well as library to accomplish responsive resizing of the path.

Additionally, SVG path syntax can be tricky, especially if you do not have a vector drawing program. An online tool I found useful in creating the demo was SvgPathEditor.

In the following demo, if you are on iOS or Safari 15.2 or less, you will see the fallback to using CSS transitions and transforms.

Michelle has also written a much more comprehensive overview of CSS Motion Path with several great demos.

Demo of offset-path animating Santa's sleigh
.sleigh-ride {
height: 180px;
overflow: hidden;
background-color: lightblue;
}

.sleigh-ride img {
offset-path: path('M -1 120 s 165 -5 346 -32 s 179 9 411 25');
animation: ride 6000ms ease-in-out var(--play-state, paused);
transform-origin: left center;
opacity: 0;
}

@supports not (offset-path: path('M 1 1')) {
.sleigh-ride img {
--sleigh-position: 100%;
--sleigh-transform: translateY(25%) rotate(12deg);

position: relative;
left: 0;
transform: translateY(150%) rotate(-12deg);
}
}

@keyframes ride {
5%, 95% {
opacity: 1;
}
100% {
offset-distance: 100%;
left: var(--sleigh-position, 0);
transform: var(--sleigh-transform, none);
opacity: 0;
}
}
document
.querySelector(".sleigh-ride img")
.addEventListener("animationend", (e) => {
const sleigh = e.target;
const sleighPlayPause = document.getElementById("animate-sleigh");

// disable animation to reset for replay
sleigh.style.animation = "none";

// reset play state and button state
document
.querySelector(".sleigh-ride")
.style.setProperty("--play-state", "paused");
sleighPlayPause.dataset.state = "paused";
sleighPlayPause.textContent = "Play animation";

// trigger reflow to successfully replay animation
sleigh.offsetWidth;
// re-enable animation
sleigh.removeAttribute("style");
});
silhouette of Santa on his sleigh with the reindeer in flight

Scroll-Linked Animations

On the cutting-edge of native CSS animation capabilities are scroll-linked animations. These animations enhance keyframe animations by attaching them to scroll position instead of the classic timeline.

Unfortunately, these are not stable in any browser but are available as an experiment in Chromium 89+ if you enable "Experimental Web Platform Features" under chrome://flags. I recommend doing this in Chrome Canary to prevent accidentally harming your normal browsing experience if you forget you have this on.

For more demos and a very thorough explanation of how scroll-linked animations work and what they're capable of, be sure to watch Bramus Van Damme presentation from CSS Cafe. Once you have the experimental flag on, you can also peruse his collection of demos on CodePen.

Demo of a scroll-linked progress indicator
@keyframes progressbar {
0% {
transform: scaleX(0);
}
100% {
transform: scaleX(1);
}
}

@scroll-timeline progressbar-timeline {
time-range: var(--progress-time);
source: selector(#scroll-animation);
}

.progress-bar {
background-color: blue;
height: 1rem;
position: sticky;
top: 0;
transform-origin: left;
animation: progressbar linear forwards var(--progress-time, 0);
animation-timeline: progressbar-timeline;
}

#scroll-animation {
position: relative;
height: max(10rem, 30vh);
overflow-y: auto;
}

.warning {
background-color: lightyellow;
border: 2px solid orange;
padding: 1rem;
}

@supports (animation-timeline: yes) {
.warning {
display: none;
}

.progress-bar {
--progress-time: 1s;
}
}

⚠️ Your browser does not support CSS Scroll-Linked Animations. View this demo by using Chromium 89+ with “Experimental Web Platform Features” enabled.

Lorem ipsum, dolor sit amet consectetur adipisicing elit. Maxime nemo accusamus velit sunt atque maiores neque!

Dicta sint, quo nesciunt expedita quisquam id ipsum quaerat minima voluptate aut in, numquam unde? Expedita.

Nihil itaque esse, molestiae soluta minus earum temporibus dignissimos hic. Ab provident quasi voluptatem distinctio odit!

Labore ipsa architecto provident ullam, molestias assumenda obcaecati! Libero quidem doloremque voluptatum repellendus cumque facere sit!

Quas animi recusandae corrupti repellat culpa dolore, temporibus repudiandae omnis eum atque distinctio tempora ullam consequatur.

Ipsam molestiae deleniti cumque, maxime, error nulla sunt veniam corporis qui laboriosam quas excepturi voluptatibus laborum!

Unde iste repellat accusamus, quibusdam corporis placeat velit, cumque dolorum labore voluptas eligendi porro saepe rerum.

Laboriosam in error ipsam, dolorem aliquam magnam officiis totam exercitationem doloribus dicta excepturi incidunt unde earum!

Quas animi recusandae corrupti repellat culpa dolore, temporibus repudiandae omnis eum atque distinctio tempora ullam consequatur.

Ipsam molestiae deleniti cumque, maxime, error nulla sunt veniam corporis qui laboriosam quas excepturi voluptatibus laborum!

Unde iste repellat accusamus, quibusdam corporis placeat velit, cumque dolorum labore voluptas eligendi porro saepe rerum.

Laboriosam in error ipsam, dolorem aliquam magnam officiis totam exercitationem doloribus dicta excepturi incidunt unde earum!

Bonus: 3D CSS animation

Transforms also provide the opportunity to create 3D animation.

Get inspired by these jaw-dropping 3D animations created by Amit Sheen. Then check out Amit walking through how to create 3D shapes with Kevin Powell and begin animating them.