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%;}