Kinetic
CSS
Emiliano Pisu
Father of 2 lovely "monsters"
D&D master, player ... whatever
Senior Design Engineer, Sensei & Co-Host
@ Dev Dojo IT
linkedin.com/in/pixu1980
github.com/pixu1980
codepen.io/pixu1980
linkedin.com/company/dev-dojo-it/
youtube.com/@devdojo_it
twitch.tv/devdojo_it
@devdojo_it
Modern CSS motion APIs for choreography without JavaScript
A fast tour through the new CSS primitives that power transitions, timelines, and reactive motion.
What are we going to
talk about today?
Agenda
- @starting-style for entry animations
- A motion architecture blueprint
- Performance rules for smooth motion
- Scroll driven reveals with animation-timeline: view()
- A gaussian curve with exp() and pow()
- sibling-index() and sibling-count() for layout math
- Trigonometric positioning with sin()
- A layered tilt card (pointer driven 3D)
Starting with Our Sponsors
.sponsor-logo-container {
--stagger-base: 0.25s;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 1fr;
...
}
.sponsor-logo {
...
opacity: 1;
transform: scale(1) translateY(0);
transition:
opacity 0.3s ease-out,
transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
@starting-style {
opacity: 0;
transform: scale(0.5) translateY(40px);
}
@supports (--index: calc(sibling-index() * 1s)) {
transition-delay:
calc(sibling-index() * var(--stagger-base));
}
@supports not (--index: calc(sibling-index() * 1s)) {
/*
Fallback: explicit --i on elements
(or via nth-child rules elsewhere).
*/
transition-delay:
calc((var(--i, 0) + 1) * var(--stagger-base));
}
}
@starting-style
Entry animations without JS are finally possible thanks to @starting-style at-rule: the CSS selector defines a starting style, then transitions it to the normal styles.Motion Architecture
A "blueprint" about how CSS motion APIs layer together into a coherent system.
Foundation
@property, Custom Properties, Design Tokens
Primitives
translate, scale, rotate, offset-path, filter
Triggers
:hover, :focus, @starting-style, scroll(), view()
Composition
animation-composition, View Transitions, timeline ranges
Guardians
prefers-reduced-motion, container queries, @supports
Performance
A set of CSS properties to know what triggers layout, paint, and composition.
Composition
transformopacityfilter(GPU)clip-path(simple)
Paint
backgroundcolorbox-shadowborder-radius
Layout
width,heightmargin,paddingtop,leftfont-size
Scroll-driven Container
with Appearing Cards Logos
42DE
Andrea Carratta
Arrow Worm
Ciabot
Fattutto
Geeks Creations
Hotel Cube
Kaysoft
Namirial
Nopos
Orbyta Tech
Quantik Mind
Storyblok
Taktik Flowr
Torino .NET
Welyk
@supports (animation-timeline: view(inline)) {
.scroll-view-card {
animation: reveal-and-elevate 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) both;
/* Precise control over animation range */
animation-range:
entry 35% /* start after element enters from right by its 35% */
cover 70%; /* end 70% through cover phase */
animation-timeline: view(inline);
}
}
@supports not (animation-timeline: view(inline)) {
.scroll-view-card {
opacity: 1;
transform: scale(1) translateY(0);
}
}
@keyframes reveal-and-elevate {
0% {
opacity: 0;
transform: scale(0.85) translateY(8rem);
filter: blur(8px);
}
50% { filter: blur(4px); }
100% {
opacity: 1;
transform: scale(1) translateY(0);
filter: blur(0);
}
}
scroll-driven Animations
The animation-timeline, scroll-timeline, and view-timeline CSS properties advance animations based on the user's scroll position. Here using animation-timeline: view(inline) for native scroll-driven effects, no JavaScript required.
Gaussian-like Curve with our
"breathing" 16 Sponsors
.gaussian-container {
--curve-width: 90%;
--curve-height: 35rem;
--logo-size: 7rem;
/* Gaussian parameters: μ = 50 (center), σ = 18 (spread) */
--mu: 50;
--sigma: 18;
position: relative;
width: var(--curve-width);
height: calc(var(--curve-height) + var(--logo-size) + 2.5rem);
}
/* Individual logo - uses sibling-index() and sibling-count() for dynamic positioning
Gaussian y-value computed at runtime using the same formula as the curve shape */
.gaussian-logo {
/* Calculate x position as percentage (0 to 100) based on index
sibling-index() is 1-based, so subtract 1 for 0-based positioning */
--x-percent: calc((sibling-index() - 1) / (sibling-count() - 1) * 100);
/* Calculate gaussian y-value at runtime: f(x) = e^(-(x-μ)²/(2σ²)) */
--gaussian-y: exp(calc(-1 * pow(var(--x-percent) - var(--mu), 2) / (2 * pow(var(--sigma), 2))));
position: absolute;
left: calc(var(--x-percent) * 1%);
bottom: calc(var(--gaussian-y) * var(--curve-height));
width: var(--logo-size);
height: var(--logo-size);
transform:
translate(-50%, 50%)
scale(calc(
(0.7 + var(--gaussian-y) * 0.3) *
var(--logo-breath-scale, 1)
));
...
}
.gaussian-logo {
...
transform:
translate(-50%, 50%)
scale(calc(
(0.7 + var(--gaussian-y) * 0.3) *
var(--logo-breath-scale, 1)
));
animation: gaussian-breath 4s linear infinite;
animation-delay: calc((sibling-index() - 1) * .5s);
animation-fill-mode: both;
}
@property --logo-breath-scale {
syntax: '<number>';
initial-value: 1;
inherits: false;
}
@keyframes gaussian-breath {
0%,
100% {
--logo-breath-scale: 1;
}
50% {
--logo-breath-scale: 1.2;
}
}
Advanced with CSS Math
Position elements along a bell curve using CSS exp() and pow() functions: zero SVG, zero JavaScript... just pure stylesheet magic.
Our Sponsors along a
sneaking sinusoidal wave
.trigonometric-sponsors-wave {
--wave-length: 360deg;
--wave-amplitude: 15rem;
--steps: calc(sibling-count() - 1);
--scale-min: 0.8;
--scale-max: 1.25;
--logo-size-min: 12rem;
--logo-gap: clamp(0.4rem, 0.9vw, 0.6rem);
...
}
.sponsor-wave-item {
--index: calc(sibling-index() - 1);
width: var(--logo-size-min);
height: var(--logo-size-min);
left: calc((var(--index) / var(--steps)) * 100%);
transform:
translateX(calc(-50% + var(--index) * var(--logo-gap)))
translateY(-50%);
will-change: left, transform;
animation: sponsor-travel 12s ease-in-out infinite;
}
/* Move each logo along the wave by shifting its parameter (index + travel).
We animate real properties (left/transform/filter), so it stays smooth without @property registration. */
@keyframes sponsor-travel {
0%,
100% {
/* travel = -3 (can go out of bounds, container clips it) */
left: calc(((var(--index) - 3) / var(--steps)) * 100%);
transform:
translateX(calc(-50% + var(--index) * var(--logo-gap)))
translateY(calc(-50% + var(--wave-amplitude) * sin(calc((var(--index) - 3) / var(--steps) * var(--wave-length)))))
scale(calc(var(--scale-min) + (var(--scale-max) - var(--scale-min)) * ((sin(calc((var(--index) - 3) / var(--steps) * var(--wave-length))) + 1) / 2)));
}
50% {
/* travel = +3 */
left: calc(((var(--index) + 3) / var(--steps)) * 100%);
transform:
translateX(calc(-50% + var(--index) * var(--logo-gap)))
translateY(calc(-50% + var(--wave-amplitude) * sin(calc((var(--index) + 3) / var(--steps) * var(--wave-length)))))
scale(calc(var(--scale-min) + (var(--scale-max) - var(--scale-min)) * ((sin(calc((var(--index) + 3) / var(--steps) * var(--wave-length))) + 1) / 2)));
}
}
Trigonometric Positioning
Use sin(), cos(), and tan() to create circular orbits, wave patterns, and dynamic spatial arrangements. Transform Cartesian geometry into visual choreography.
Sponsors in two circles
(NOTE: works well in chromium based browsers only)
.sibling-logo {
/* arc angle per logo uses asin() to calculate
angle subtended by each logo+gap at circle center */
--sibling-arc-angle: calc(2 * asin((var(--sibling-logo-size) + var(--sibling-logo-gap)) / (2 * var(--sibling-circle-radius))) - 1turn / sibling-count());
/* logo circle radius calculated responsively
using min() and container query units */
--sibling-circle-radius: min(50cqw - var(--sibling-logo-size) / 2, (var(--sibling-logo-size) + var(--sibling-logo-gap)) / (2 * sin(0.5turn / sibling-count())));
/* current logo angle dividing full rotation
by sibling-count() to distribute items evenly */
--sibling-current-angle: calc(1turn * sibling-index() / sibling-count() + var(--sibling-expand-offset, 0deg));
/* next logo angle used to create smooth visual
transition between adjacent logos */
--sibling-next-angle: calc(1turn * (sibling-index() + 1) / sibling-count() + var(--sibling-next-expand-offset, 0deg));
/* transform chain:
rotate → translate to radius → counter-rotate
this positions element on circle while keeping image upright */
transform:
rotate(calc(-1 * var(--sibling-current-angle)))
translate(var(--sibling-circle-radius))
rotate(var(--sibling-current-angle));
/* smooth animation of position and next-angle on hover */
transition:
--sibling-current-angle 0.4s linear,
--sibling-next-angle 0.4s linear;
/* radial mask creates smooth circle boundary without harsh edges
uses trigonometric coordinates (cos/sin) to calculate
mask gradient position based on current and next element angles */
mask: radial-gradient(
50% 50% at calc(50% + var(--sibling-circle-radius) * (cos(var(--sibling-next-angle)) - cos(var(--sibling-current-angle)))) calc(50% + var(--sibling-circle-radius) * (sin(var(--sibling-current-angle)) - sin(var(--sibling-next-angle)))),
#fff0 calc(100% - 1px + var(--sibling-logo-gap)),
#fff calc(100% + var(--sibling-logo-gap))
);
...
}
sibling-index() and sibling-count()
Sibling functions allow CSS to know the position and total count of elements in their parent. No JavaScript neededAll I have left is to wish you...
2026
2026
2026
2026
2026
2026
2026
2026
2026
2026
Te pensavi
eh?
@property --tilt-card--pos-x {
syntax: '<number>';
inherits: true;
initial-value: 0;
}
@property --tilt-card--pos-y {
syntax: '<number>';
inherits: true;
initial-value: 0;
}
.card-container {
--tilt-card--perspective: 50rem;
--tilt-card--pos-x: 0;
--tilt-card--pos-y: 0;
perspective: var(--tilt-card--perspective);
transition:
--tilt-card--pos-x 800ms cubic-bezier(0.2, 0.85, 0.3, 1),
--tilt-card--pos-y 800ms cubic-bezier(0.2, 0.85, 0.3, 1);
...
}
.card-container * {
transform-style: preserve-3d;
}
.card {
--tilt-card--x: calc(var(--tilt-card--pos-y, 0.1) * -120deg);
--tilt-card--y: calc(var(--tilt-card--pos-x, 0.1) * 120deg);
transform: rotateX(var(--tilt-card--x)) rotateY(var(--tilt-card--y));
transition: transform 800ms cubic-bezier(0.2, 0.85, 0.3, 1);
...
}
.card-content {
overflow: clip;
backface-visibility: hidden;
filter: drop-shadow(0 0 15px rgba(0, 245, 255, 0.6));
perspective: calc(cos(var(--tilt-card--x)) * cos(var(--tilt-card--y)) * var(--tilt-card--perspective));
perspective-origin: calc(50% - cos(var(--tilt-card--x)) * sin(var(--tilt-card--y)) * var(--tilt-card--perspective)) calc(50% + sin(var(--tilt-card--x)) * var(--tilt-card--perspective));
...
}
.card-layer {
transform: translateZ(calc((sibling-index() - 1) * -2rem));
...
}
registered @property
Differently than normal custom properties, the ones registered via @property at-rule are transitionable and animatable, allowing to calculate interpolation between all the specified values.Wrapping up
With modern CSS Motion APIs it's possible to create small scenes.
Motion is choreography: state, timing, and meaning.
Use CSS as the primary language, then add JavaScript only for input and orchestration.
- Start with entrance, scroll reveal, or state change
- Make values editable with custom properties
- Add progressive enhancement with @supports
Slides Links
© 1987-2025 - Warner Bros. Entertainment Inc.
Grazie ❤️!