01. @keyframes
Define the animation’s steps with @keyframes. Use from/to for a simple A→B animation, or percentage stops for multi-step sequences.
Preview style.css index.html
@keyframes bounce {
0% , 100% {
transform : translateX ( 0 );
animation-timing-function : ease-in ;
}
50% {
transform : translateX ( 100 % );
animation-timing-function : ease-out ;
}
}
.ball {
width : 3 rem ; height : 3 rem ;
background : #6366f1 ;
border-radius : 50 % ;
animation : bounce 1 s infinite ;
}
/* from / to — simplest form */
@keyframes fade {
from { opacity : 1 ; }
to { opacity : 0 ; }
}
/* percentage stops — multi-step */
@keyframes bounce {
0% { transform : translateY ( 0 ); }
50% { transform : translateY ( -24 px ); }
100% { transform : translateY ( 0 ); }
}
.el {
animation-name : bounce;
animation-duration : 0.6 s ;
}
02. Duration & Timing Function
The same keyframes with different durations and timing functions produce very different feelings.
Preview style.css index.html
@keyframes slide {
from { transform : translateX ( 0 ); }
to { transform : translateX ( 180 px ); }
}
.rows {
display : flex ;
flex-direction : column ;
gap : .6 rem ;
}
.row {
display : flex ;
align-items : center ;
gap : .75 rem ;
}
.row-label {
width : 12 rem ;
font-size : .7 rem ;
font-family : monospace ;
color : #64748b ;
flex-shrink : 0 ;
}
.dot {
width : 2 rem ;
height : 2 rem ;
background : #6366f1 ;
border-radius : 50 % ;
animation : slide infinite alternate ;
}
.d-short {
animation-duration : .3 s ;
animation-timing-function : ease-out ;
}
.d-medium {
animation-duration : 1 s ;
animation-timing-function : ease-in-out ;
}
.d-long {
animation-duration : 2 s ;
animation-timing-function : ease ;
}
.d-linear {
animation-duration : 1 s ;
animation-timing-function : linear ;
}
.d-spring {
animation-duration : .8 s ;
animation-timing-function : cubic-bezier ( 0.34 , 1.56 , 0.64 , 1 );
} < div class = "rows" >
< div class = "row" >
< span class = "row-label" >0.3s ease-out</ span >
< div class = "dot d-short" ></ div >
</ div >
< div class = "row" >
< span class = "row-label" >1s ease-in-out</ span >
< div class = "dot d-medium" ></ div >
</ div >
< div class = "row" >
< span class = "row-label" >2s ease</ span >
< div class = "dot d-long" ></ div >
</ div >
< div class = "row" >
< span class = "row-label" >1s linear</ span >
< div class = "dot d-linear" ></ div >
</ div >
< div class = "row" >
< span class = "row-label" >0.8s spring</ span >
< div class = "dot d-spring" ></ div >
</ div >
</ div >
/* animation shorthand */
/* name duration timing-fn delay iteration direction fill-mode */
animation: slide 0 .4s ease-out 0s 1 normal forwards;
/* the four most important values */
animation: slide 0 .4s ease-out forwards;
animation-timing-function : ease;
animation-timing-function : cubic-bezier(0 .34 , 1 .56 , 0 .64 , 1); /* spring */
animation-timing-function : steps(8, end); /* choppy/sprite */
03. Delay & Iteration
Use animation-delay to stagger animations. animation-iteration-count: infinite loops forever.
Preview style.css index.html script.js
@keyframes pop {
0% { transform : scale ( .5 ); opacity : 0 ; }
60% { transform : scale ( 1.15 ); }
100% { transform : scale ( 1 ); opacity : 1 ; }
}
.di-row {
display : flex ;
gap : .75 rem ;
align-items : center ;
margin-bottom : 1 rem ;
cursor : pointer ;
}
.dot {
width : 2.25 rem ;
height : 2.25 rem ;
background : #6366f1 ;
border-radius : 50 % ;
animation : pop .5 s ease-out both ;
animation-play-state : paused ;
}
.di-row.playing .dot { animation-play-state : running ; }
.dot:nth-child ( 1 ) { animation-delay : 0 ms ; }
.dot:nth-child ( 2 ) { animation-delay : 100 ms ; }
.dot:nth-child ( 3 ) { animation-delay : 200 ms ; }
.dot:nth-child ( 4 ) { animation-delay : 300 ms ; }
.dot:nth-child ( 5 ) { animation-delay : 400 ms ; }
.iter-label {
font-size : .75 rem ;
color : #94a3b8 ;
font-family : monospace ;
margin-bottom : .4 rem ;
}
@keyframes spin-iter { to { transform : rotate ( 360 deg ); } }
.iter-box {
display : inline-block ;
width : 2 rem ;
height : 2 rem ;
background : #a855f7 ;
border-radius : 4 px ;
animation : spin-iter 1 s linear 3 ;
}
.btn {
padding : .4 rem .9 rem ;
background : #6366f1 ;
color : white ;
border : none ;
border-radius : 6 px ;
cursor : pointer ;
font-size : .8 rem ;
} < div class = "iter-label" >stagger: start animation with progressive delay</ div >
< div class = "di-row" id = "di-row" >
< div class = "dot" ></ div >
< div class = "dot" ></ div >
< div class = "dot" ></ div >
< div class = "dot" ></ div >
< div class = "dot" ></ div >
</ div >
< button class = "btn" id = "di-play" >Replay stagger</ button >
< hr >
< div class = "iter-wrap" >
< div class = "iter-label" >iteration-count: 3 — spins exactly 3 times</ div >
< div class = "iter-box" id = "di-iter" ></ div >
< button class = "btn" id = "di-iter-replay" >Replay</ button >
</ div > function replayRow () {
const row = document. getElementById ( 'di-row' );
row.classList. remove ( 'playing' );
row. querySelectorAll ( '.dot' ). forEach (( dot ) => {
dot.style.animation = 'none' ;
void dot.offsetWidth;
dot.style.animation = '' ;
});
row.classList. add ( 'playing' );
}
function replayIter () {
const el = document. getElementById ( 'di-iter' );
el.style.animation = 'none' ;
void el.offsetWidth;
el.style.animation = '' ;
}
document. getElementById ( 'di-play' ).onclick = replayRow;
document. getElementById ( 'di-iter-replay' ).onclick = replayIter;
animation-delay : 0.2s;
animation-delay : -0.5s; /* negative starts mid-animation (skips ahead) */
/* stagger siblings */
.item:nth-child ( 1 ) { animation-delay : 0 ms ; }
.item:nth-child ( 2 ) { animation-delay : 100 ms ; }
.item:nth-child ( 3 ) { animation-delay : 200 ms ; }
animation-iteration-count : 1; /* plays once */
animation-iteration-count : 3; /* plays 3 times */
animation-iteration-count : infinite; /* loops forever */
/* always respect user preferences */
@media (prefers-reduced-motion: reduce) {
* { animation-duration : 0.01 ms !important ; }
}
04. Direction & Fill-mode
alternate reverses on every other iteration — smooth loops without an abrupt reset. forwards keeps the final keyframe state when the animation ends.
Preview style.css index.html script.js
@keyframes slide-right {
from { transform : translateX ( 0 ); }
to { transform : translateX ( 160 px ); }
}
.rows {
display : flex ;
flex-direction : column ;
gap : .75 rem ;
}
.row {
display : flex ;
align-items : center ;
gap : .75 rem ;
}
.row-label {
width : 11 rem ;
font-size : .7 rem ;
font-family : monospace ;
color : #64748b ;
flex-shrink : 0 ;
}
.dot {
width : 2 rem ;
height : 2 rem ;
background : #6366f1 ;
border-radius : 50 % ;
animation : slide-right 1.2 s ease-in-out 1 both paused ;
}
.dir-normal { animation-direction : normal ; }
.dir-reverse { animation-direction : reverse ; }
.dir-alternate { animation-direction : alternate ; animation-iteration-count : 4 ; }
.fill-none { animation-fill-mode : none ; animation-direction : normal ; }
.fill-fwd { animation-fill-mode : forwards ; }
.fill-both { animation-fill-mode : both ; }
.playing .dot { animation-play-state : running ; }
.btn {
padding : .4 rem .9 rem ;
background : #6366f1 ;
color : white ;
border : none ;
border-radius : 6 px ;
cursor : pointer ;
font-size : .8 rem ;
margin-top : .75 rem ;
} < div class = "rows" id = "df-rows" >
< div class = "row" >
< span class = "row-label" >normal</ span >
< div class = "dot dir-normal" ></ div >
</ div >
< div class = "row" >
< span class = "row-label" >reverse</ span >
< div class = "dot dir-reverse" ></ div >
</ div >
< div class = "row" >
< span class = "row-label" >alternate ×4</ span >
< div class = "dot dir-alternate" ></ div >
</ div >
< div class = "row" >
< span class = "row-label" >fill: none</ span >
< div class = "dot fill-none" ></ div >
</ div >
< div class = "row" >
< span class = "row-label" >fill: forwards</ span >
< div class = "dot fill-fwd" ></ div >
</ div >
< div class = "row" >
< span class = "row-label" >fill: both</ span >
< div class = "dot fill-both" ></ div >
</ div >
</ div >
< button class = "btn" onclick = " stopAnim ()" >Play all</ button > const stopAnim = () => {
const rows = document. getElementById ( 'df-rows' );
rows.classList. remove ( 'playing' );
rows. querySelectorAll ( '.dot' ). forEach (( dot ) => {
dot.style.animation = 'none' ;
void dot.offsetWidth;
dot.style.animation = '' ;
});
void rows.offsetWidth;
rows.classList. add ( 'playing' );
}
animation-direction : normal; /* always forward */
animation-direction : reverse; /* always backward */
animation-direction : alternate; /* forward, then backward, repeat */
animation-direction : alternate-reverse ;
animation-fill-mode : none; /* snaps back to original */
animation-fill-mode : forwards; /* holds last keyframe state after end */
animation-fill-mode : backwards; /* applies first keyframe before delay */
animation-fill-mode : both;
05. play-state & prefers-reduced-motion
Toggle animation-play-state: paused to pause any running animation without removing it.
Preview style.css index.html script.js
@keyframes spin-ps { to { transform : rotate ( 360 deg ); } }
@keyframes pulse-ps {
0% , 100% { transform : scale ( 1 ); }
50% { transform : scale ( 1.3 ); }
}
.ps-demo {
display : flex ;
gap : 2 rem ;
flex-wrap : wrap ;
align-items : center ;
margin-bottom : 1 rem ;
}
.spinner {
width : 3 rem ;
height : 3 rem ;
border : 4 px solid #e2e8f0 ;
border-top-color : #6366f1 ;
border-radius : 50 % ;
animation : spin-ps 1 s linear infinite ;
}
.pulser {
width : 3 rem ;
height : 3 rem ;
background : #6366f1 ;
border-radius : 50 % ;
animation : pulse-ps 1 s ease-in-out infinite ;
}
.paused {
animation-play-state : paused !important ;
}
.btn {
padding : .4 rem .9 rem ;
border : 1 px solid #6366f1 ;
background : white ;
color : #6366f1 ;
border-radius : 6 px ;
cursor : pointer ;
font-size : .875 rem ;
}
.btn.on {
background : #6366f1 ;
color : white ;
} < div class = "ps-demo" >
< div class = "spinner" id = "ps-spin" ></ div >
< div class = "pulser" id = "ps-pulse" ></ div >
</ div >
< button class = "btn on" id = "ps-btn" onclick = " pauseAnim ()" >Pause</ button > const pauseAnim = () => {
const spin = document. getElementById ( 'ps-spin' );
const pulse = document. getElementById ( 'ps-pulse' );
const btn = document. getElementById ( 'ps-btn' );
const paused = spin.classList. toggle ( 'paused' );
pulse.classList. toggle ( 'paused' );
btn.textContent = paused ? 'Resume' : 'Pause' ;
btn.classList. toggle ( 'on' , ! paused);
}
.el { animation : spin 2 s linear infinite ; }
.el.paused { animation-play-state : paused ; }
/* JS — just toggle a class */
el .classList .toggle("paused")
@media (prefers-reduced-motion: reduce) {
/* option 1: remove animation entirely */
* { animation : none !important ; transition : none !important ; }
/* option 2: keep subtle motion, remove large movement */
.spinner { animation-duration : 10 s ; }
.slide-in { transform : none ; opacity : 1 ; }
}