Animation comparisons

13 old vs modern animation CSS techniques, side by side.

Browser compatibility:

Random values per element without JavaScript

Limited availability
Old
// JS: randomise per element at runtimedocument.querySelectorAll('.card').forEach(el => {  const r = Math.random() * 30 - 15;  el.style.setProperty('--rotate', r + 'deg');  const d = Math.random();  el.style.setProperty('--delay', d + 's');});/* CSS reads the inline vars */.card {  rotate: var(--rotate);  animation-delay: var(--delay);}
Modern
.card   {  rotate: random(-15deg, 15deg);  animation-delay: random(0s, 1s);}

Motion path animation without JavaScript

Widely available
Old
<!-- SVG in HTML --><svg><path id="track" d="M 0 0 C 150 -100 300 100 450 0"/></svg><div class="ball"></div>// gsap + motionPath plugin requiredgsap.to('.ball', {  duration: 2,  ease: 'none',  motionPath: { path: '#track', autoRotate: true }});
Modern
@keyframes along-path   {  from   {    offset-distance: 0%;  }  to     {    offset-distance: 100%;  }}.ball   {  offset-path: path("M 0 0 C 150 -100 300 100 450 0");  offset-distance: 0%;  offset-rotate: auto;  animation: along-path 2s linear infinite;}

Reduced motion without JavaScript detection

Widely available
Old
// JS: check OS preference, disable animations manuallyconst mq = window.matchMedia('(prefers-reduced-motion: reduce)');if (mq.matches) {  document.querySelectorAll('.animated').forEach(    el => el.style.animation = 'none'  );}
Modern
@media (prefers-reduced-motion: reduce)   {  *, *::before, *::after   {    animation-duration: 0.01ms !important;  }}

Custom easing curves without cubic-bezier guessing

Newly available
Old
// JS animation library for bounce/spring easinganime({  targets: el,  translateX: 300,  easing: 'spring(1, 80, 10, 0)'});
Modern
.el   {  transition: transform 0.6s    linear(0, 1.2 60%, 0.9, 1.05, 1);}

Smooth height auto animations without JavaScript

Newly available
Old
.accordion  {  overflow: hidden;}// JS: measure, set px height, then snap to autofunction open(el) {   el.style.height = el.scrollHeight + 'px';   el.addEventListener('transitionend', () => {     el.style.height = 'auto';   }, { once: true }); }
Modern
:root   {  interpolate-size: allow-keywords;}.accordion   {  height: 0;  overflow: hidden;  transition: height .3s ease;}.accordion.open   {  height: auto;}

Sticky & snapped element styling without JavaScript

Limited availability
Old
/* JS scroll listener approach */const header = document   .querySelector('.header');const offset = header   .offsetTop;window.addEventListener(   'scroll', () =>  {  header.classList       .toggle('stuck',      window.scrollY         >= offset);});
Modern
.header-wrap   {  container-type: scroll-state;}@container scroll-state( /* [!code ++] */stuck: top /* [!code ++] */)   {  .header   {    box-shadow: 0 2px 8px #0002;  }}

Responsive clip paths without SVG

Limited availability
Old
.shape   {  clip-path: path(     'M0 200 L100 0 L200 200 Z'  );}
Modern
.shape   {  clip-path: shape(     from 0% 100%,    line to 50% 0%,    line to 100% 100%  );}

Staggered animations without nth-child hacks

Limited availability
Old
/* Manual index per nth-child */li:nth-child(1)   {  --i: 0;}li:nth-child(2)   {  --i: 1;}li:nth-child(3)   {  --i: 2;}li:nth-child(4)   {  --i: 3;}/* … repeat for every item … */li   {  transition-delay: calc(0.1s * var(--i));}
Modern
li   {  transition: opacity .25s ease, translate .25s ease;  transition-delay:    calc(0.1s * (sibling-index() - 1));}

Independent transforms without the shorthand

Widely available
Old
.icon   {  transform: translateX(10px) rotate(45deg) scale(1.2);}.icon:hover   {  transform: translateX(10px) rotate(90deg) scale(1.2);
Modern
.icon   {  translate: 10px 0;  rotate: 45deg;  scale: 1.2;}.icon:hover   {  rotate: 90deg;}

Animating display none without workarounds

Newly available
Old
.panel  {  transition: opacity .2s;}.panel.hidden  {  opacity: 0;  visibility: hidden;}// JS: after transition, set display:none and pointer-eventsel.addEventListener('transitionend', () => {   el.style.display = 'none'; });
Modern
.panel   {  transition: opacity .2s, overlay .2s allow-discrete;  transition-behavior: allow-discrete;}.panel.hidden   {  opacity: 0;  display: none;}

Entry animations without JavaScript timing

Newly available
Old
.card   {  opacity: 0;  transform: translateY(10px);}.card.visible   {  opacity: 1;  transform: none;}// JS: must run after paint or transition won't run requestAnimationFrame(() => {   requestAnimationFrame(() => {     el.classList.add('visible');   });});
Modern
.card   {  transition: opacity .3s, transform .3s;  @starting-style   {    opacity: 0;    transform: translateY(10px);  }}

Page transitions without a framework

Newly available
Old
// barba.js or custom router transitionsimport Barba from '@barba/core';Barba.init({  transitions: [{    leave: ({ current }) => gsap.to(current.container, { opacity: 0 }),    enter: ({ next }) => gsap.from(next.container, { opacity: 0 })  }]});
Modern
// JS: wrap DOM updatedocument.startViewTransition(() => {  document.body.innerHTML = newContent;});.hero {  view-transition-name: hero;}/* Optional: ::view-transition-old/new(hero) */

Scroll-linked animations without a library

Limited availability
Old
// scroll-reveal.jsconst obs = new IntersectionObserver(  (entries) => {    entries.forEach(e => {      if (e.isIntersecting)        e.target.classList.add('visible');    });  });document.querySelectorAll('.reveal')  .forEach(el => obs.observe(el));
Modern
@keyframes reveal   {  from   {    opacity: 0;    translate: 0 40px;  }  to     {    opacity: 1;    translate: 0 0;  }}.reveal   {  animation: reveal linear both;  animation-timeline: view();  animation-range: entry 0% entry 100%;}

Other categories

ESC