CSS Animations

TheTechnoCult
4 min readNov 12, 2023

--

Source of refs: https://dribbble.com/tags/css-animation

Easings: https://easings.net

The easing configuration is accessible via the dev tools:

(!) What to animate — YES: transform, opacity, color, background. NO: height, width, left, right, font-size etc.

Animation

Controlling via the dev tools.

(!) use animation-play-state: paused by :hover to stop animation.

Choreography — multiple elements coordinating with each other.

Animation State Machine: https://help.rive.app/editor/state-machine

FLIP animation technique. In case of the layout animation (height, width, margins etc) make sense to use the FLIP animation technique. FLIP technique calculates the first and last position/size of an element and uses transform properties to create the animation. https://css-tricks.com/animating-layouts-with-the-flip-technique

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Layout</title>
<link rel="stylesheet" href="style.final.scss" />
</head>
<body>
<div id="app" data-state="collapsed">
<figure class="ui-figure" data-flip-bg>
<figcaption class="ui-caption">Animation</figcaption>
</figure>
<div class="ui-content">
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Inventore
ullam hic consectetur ducimus neque ipsam incidunt voluptatem
voluptatum eos. Voluptatum minus omnis provident sit architecto,
mollitia nihil aspernatur sed praesentium.
</p>
</div>
</div>
</body>
<script>
const figureEl = document.querySelector('.ui-figure');
const captionEl = document.querySelector('.ui-caption');
const appEl = document.querySelector('#app');

appEl.addEventListener('click', () => {
flip(() => {
app.dataset.state =
app.dataset.state === 'collapsed' ? 'expanded' : 'collapsed';
}, [figureEl, captionEl]);
});

function flip(fn, firstEls, lastEls = firstEls) {
const firstRects = firstEls.map((el) => el.getBoundingClientRect());

fn();

requestAnimationFrame(() => {
const lastRects = lastEls.map((el) => el.getBoundingClientRect());

lastRects.forEach((lastRect, i) => {
const firstRect = firstRects[i];
const lastEl = lastEls[i];

if (!firstRect) {
return;
}

const dx = firstRect.x - lastRect.x;
const dy = firstRect.y - lastRect.y;
const dw = firstRect.width / lastRect.width;
const dh = firstRect.height / lastRect.height;

lastEl.style.setProperty('--dx', dx);
lastEl.style.setProperty('--dy', dy);
lastEl.style.setProperty('--dw', dw);
lastEl.style.setProperty('--dh', dh);
lastEl.dataset.flip = 'invert';

requestAnimationFrame(() => {
lastEl.dataset.flip = 'play';
});
});
});
}
</script>
</html>
@import '../styles/base.scss';

body {
display: flex;
justify-content: center;
align-items: center;
}

#app {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr;
grid-template-areas: 'figure';
grid-column-gap: 2rem;
width: 20vw;
aspect-ratio: 1;
border: 1px solid;

&[data-state='collapsed'] {
.ui-content {
display: none;
}
}

&[data-state='expanded'] {
padding: 2rem;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr;
grid-template-areas: 'figure content';
aspect-ratio: 2;
width: 70vw;
}
}

.ui-content {
grid-area: content;
animation: content 0.3s 0.3s both;
}

@keyframes content {
from {
opacity: 0;
transform: translateX(-2rem);
}
to {
opacity: 1;
transform: none;
}
}

.ui-figure {
display: flex;
justify-content: center;
align-items: center;

&:before {
content: '';
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
background: linear-gradient(to bottom right, #124269, #309aef);
transform-origin: top left;
}

&[data-flip='invert']:before {
transform: translate(calc(var(--dx) * 1px), calc(var(--dy) * 1px))
scale(var(--dw), var(--dh));
}

&[data-flip='play']:before {
transition: transform 0.3s;
transform: none;
}

> .ui-caption {
font-size: 2rem;
font-weight: bold;
color: white;
}
}

[data-flip] {
&[data-flip-bg][data-flip='invert']:before,
&:not([data-flip-bg])[data-flip='invert'] {
transform: translate(calc(var(--dx) * 1px), calc(var(--dy) * 1px))
scale(var(--dw), var(--dh));
}

&[data-flip-bg][data-flip='play']:before,
&:not([data-flip-bg])[data-flip='play'] {
transition: transform 0.3s;
transform: none;
}
}

Reactive Animations

Animation that depends on the continuous input, changes based on events.

Example: the pointermove event using the lerp technique. “Lerp,” or linear interpolation, is a linear equation that expresses the straight line between two [x, y] points on a plane as a function y = f(x). In this case, lerp allows for achieving smooth animation.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Reactive</title>
<link rel="stylesheet" href="style.final.scss" />
</head>
<body>
<div class="circle"></div>
</body>
<script>
const circleEl = document.querySelector('.circle');

const currentPoint = { x: 0, y: 0 };
const targetPoint = { x: 0, y: 0 };

function lerp() {
currentPoint.x = currentPoint.x + (targetPoint.x - currentPoint.x) * 0.1;
currentPoint.y = currentPoint.y + (targetPoint.y - currentPoint.y) * 0.1;

circleEl.style.setProperty('--x', currentPoint.x);
circleEl.style.setProperty('--y', currentPoint.y);

requestAnimationFrame(lerp);
}

requestAnimationFrame(lerp);

document.body.addEventListener('pointermove', (event) => {
targetPoint.x = event.clientX;
targetPoint.y = event.clientY;
});
</script>
</html>
@import '../styles/base.scss';

.circle {
height: 5vmin;
width: 5vmin;
border-radius: 50%;
background-color: blue;

transform: translate(
calc((var(--x) * 1px) - 50%),
calc((var(--y) * 1px) - 50%)
);
}

(!) It is important to have the ability to switch off the animation.

/* reduce or no-preference */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0s !important;
transition-duration: 0s !important;
}
}

Clip path animation playground: https://bennettfeely.com/clippy

Examples:

Linear-style Cursor Glow: https://codepen.io/davidkpiano/pen/gOoNZNe

Circular Mask Transition: https://codepen.io/team/keyframers/pen/jRaLgX

Diamond Slider: https://codepen.io/team/keyframers/pen/ExWGGpX

--

--

No responses yet