Animation comparisons

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

Browser compatibility:

Animate <details> with ::details-content and interpolate-size

Newly available
Old way
const details = document.querySelectorAll('details');details.forEach((d) => {  const content = d.querySelector('.content');  d.addEventListener('toggle', () => {    if (d.open) {      const h = content.scrollHeight;      content.style.height = '0px';      requestAnimationFrame(() => { content.style.height = h + 'px'; });    } else {      content.style.height = content.scrollHeight + 'px';      requestAnimationFrame(() => { content.style.height = '0px'; });    }  });});
Modern
:root {  interpolate-size: allow-keywords;}details::details-content   {  height: 0;  overflow: clip;  transition: height .3s ease, content-visibility .3s ease allow-discrete;}details[open]::details-content   {  height: auto;}/* native details, animated open and close */

CSS random() Function for Per-Element Random Values

Limited availability
Old way
// 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);}

CSS Motion Path Animation with offset-path

Widely available
Old way
<!-- 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;}

CSS prefers-reduced-motion Media Query

Widely available
Old way
// 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 CSS Easing with linear() Timing Function

Newly available
Old way
// 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);}

Animate height: auto in CSS with interpolate-size

Newly available
Old way
.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;}

CSS scroll-state() Container Queries: :stuck and :snapped

Limited availability
Old way
/* 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 CSS clip-path with the shape() Function

Limited availability
Old way
.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 CSS Animations with sibling-index()

Limited availability
Old way
/* 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));}

CSS Individual Transform Properties: translate, rotate, scale

Widely available
Old way
.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;}

Animate display: none in CSS (No JavaScript)

Newly available
Old way
.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;}

CSS Entry Animations with @starting-style

Newly available
Old way
.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);  }}

CSS View Transitions API (No Framework)

Newly available
Old way
// 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-Driven Animations in CSS with scroll-timeline

Limited availability
Old way
// 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