CSS comparisons

80 old vs modern 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%;}

Name-only @container queries without size conditions

Newly available
Old way
.sidebar {  container-name: sidebar;}.card {  display: grid;}/* size condition required even when you only care about the name */@container sidebar (width > 0)   {  .card {    grid-auto-flow: column;  }}
Modern
.sidebar {  container-name: sidebar;}.card {  display: grid;}@container sidebar   {  .card {    grid-auto-flow: column;  }}/* no dummy size condition */

CSS shape() Function: Responsive clip-path Shapes

Limited availability
Old way
/* path() uses absolute px — not responsive */.hero   {  clip-path: path(     "M 0 0 L 800 0 L 800 360 /* */  Q 400 420 0 360 Z" /* */  );  /* Breaks on resize */}
Modern
.hero   {  clip-path: shape(     from 0% 0%,    line to 100% 0%,    line to 100% 80%,    curve to 0% 80% via 50% 105%,    close   );}

Data-Driven CSS Layouts with Typed attr()

Widely available
Old way
.bar  {  block-size: 1.5rem;  inline-size: calc(1% * var(--pct));  background: var(--accent);}/* --pct set in JS per element */bars.forEach(bar =>  {  bar.style.setProperty('--pct', bar.dataset.pct);});
Modern
.bar  {  block-size: 1.5rem;  background: var(--accent);  inline-size: attr(data-pct percentage);}

CSS zoom Property vs transform: scale()

Newly available
Old way
.thumbnail   {  transform: scale(0.5);  transform-origin: top left;  margin-bottom: -50%;  margin-right: -50%;}
Modern
.thumbnail   {  zoom: 0.5;}

Prevent Scrollbar Layout Shift: scrollbar-gutter: stable

Newly available
Old way
/* Option 1: always show scrollbar (ugly on short pages) */body   {  overflow-y: scroll;}/* Option 2: hardcode scrollbar width (fragile) */body   {  padding-right: 17px;}/* scrollbar width varies by OS and browser */
Modern
body   {  scrollbar-gutter: stable;}

CSS overscroll-behavior: contain (No JavaScript)

Widely available
Old way
.modal-content   {  overflow-y: auto;}// JS: prevent scroll chaining on wheel and touch modal.addEventListener('wheel', (e) => {   e.preventDefault(); }, { passive: false }); // also needed for touch: touchmove listener
Modern
.modal-content   {  overflow-y: auto;  overscroll-behavior: contain;}

Style Scrollbars in CSS: scrollbar-color, scrollbar-width

Newly available
Old way
/* Chrome + Safari only — Firefox ignores all of this */::-webkit-scrollbar   {  width: 8px;}::-webkit-scrollbar-track   {  background: #f1f1f1;}::-webkit-scrollbar-thumb   {  background: #888;}::-webkit-scrollbar-thumb:hover   {  background: #555;}
Modern
*   {  scrollbar-width: thin;  scrollbar-color: #888 transparent;}

CSS dvh, svh, lvh: Mobile Viewport Height Fix

Widely available
Old way
.hero   {  height: 100vh;}
Modern
.hero   {  height: 100dvh;}

CSS Media Query Range Syntax (width < 768px)

Widely available
Old way
@media (min-width: 600px) and (max-width: 1200px)   {  .card   {    grid-template-columns: 1fr 1fr;  }}
Modern
@media (600px <= width <= 1200px)   {  .card   {    grid-template-columns: 1fr 1fr;  }}

Responsive Images in CSS with object-fit: cover

Widely available
Old way
<!-- div instead of img: no alt, not semantic --><div class="card-image"></div>.card-image   {  background-image: url(photo.jpg);  background-size: cover;  background-position: center;}
Modern
<img src="photo.jpg" alt="..." loading="lazy">img   {  object-fit: cover;  width: 100%;  height: 200px;}

Auto-Resize Textarea in CSS: field-sizing: content

Limited availability
Old way
textarea  {  overflow: hidden;}// JS: reset height then set to scrollHeight on every inputel.addEventListener('input', () => {   el.style.height = 'auto';   el.style.height = el.scrollHeight + 'px'; });
Modern
textarea   {  field-sizing: content;  min-height: 3lh;}

CSS corner-shape: Squircle, Scoop, and Notch Corners

Limited availability
Old way
.card   {  clip-path: polygon(     0% 10%,    2% 4%,    4% 2%,    10% 0%,    /* ...16 more points */  );}/* Or use an SVG mask image */
Modern
.card   {  border-radius: 2em;  corner-shape: squircle;}

Fill Available Space in CSS with the stretch Keyword

Limited availability
Old way
.full   {  width: 100%;  width: calc(100% - 40px);}
Modern
.full   {  width: stretch;}

CSS-Only Carousel Navigation with scroll-marker

Limited availability
Old way
/* JS: Swiper.js carousel with nav + dots */import Swiper from 'swiper';new Swiper('.carousel',   {  navigation:   {    nextEl: '.next',    prevEl: '.prev',  }  ,  pagination:   {    el: '.dots'  }  ,});/* + custom CSS for buttons, dots, states */
Modern
.carousel::scroll-button(left)   {  content: "⬅" / "Scroll left";}.carousel::scroll-button(right)   {  content: "➡" / "Scroll right";}.carousel   {  scroll-marker-group: after;}.carousel li::scroll-marker   {  content: '';  width: 10px;  height: 10px;  border-radius: 50%;}

CSS Hover Tooltips with popover=hint and interestfor

Limited availability
Old way
/* JS: manage hover, focus, and position */btn.addEventListener('mouseenter', () =>   {  tip.hidden = false;  positionTooltip(btn, tip);});btn.addEventListener('mouseleave', () =>   {  tip.hidden = true;});btn.addEventListener('focus', /* … */);btn.addEventListener('blur', /* … */);
Modern
<button interestfor="tip">  Hover me</button><div id="tip" popover=hint>  Tooltip text</div>

CSS inset Shorthand for top, right, bottom, left

Widely available
Old way
.overlay   {  position: absolute;  top: 0;  right: 0;  bottom: 0;  left: 0;}
Modern
.overlay   {  position: absolute;  inset: 0;}

CSS Anchor Positioning for Tooltips

Limited availability
Old way
/* JS: getBoundingClientRect for trigger + tooltip,   compute top/left, handle scroll/resize */.tooltip   {  position: fixed;  top: var(--computed-top);  left: var(--computed-left);}
Modern
.trigger   {  anchor-name: --tip;}.tooltip   {  position-anchor: --tip;  top: anchor(bottom);}

CSS Scroll Snap: scroll-snap-type and scroll-snap-align

Widely available
Old way
// carousel.js + Slick/Swiperimport Swiper from 'swiper';new Swiper('.carousel', {  slidesPerView: 1,  loop: true});
Modern
.carousel   {  scroll-snap-type: x mandatory;  overflow-x: auto;  display: flex;  gap: 1rem;}.carousel > *   {  scroll-snap-align: start;}

CSS Logical Properties: RTL Layouts Without left and right

Widely available
Old way
.box   {  margin-left: 1rem;  padding-right: 1rem;}[dir="rtl"] .box   {  margin-left: 0;  margin-right: 1rem;}
Modern
.box   {  margin-inline-start: 1rem;  padding-inline-end: 1rem;  border-block-start: 1px solid;}

CSS grid-template-areas: Named Grid Areas

Widely available
Old way
.header   {  grid-column: 1 / -1;}.sidebar   {  grid-column: 1;  grid-row: 2;}.main   {  grid-column: 2;  grid-row: 2;}.footer   {  grid-column: 1 / -1;  grid-row: 3;}
Modern
.layout   {  display: grid;  grid-template-areas:    "header header"    "sidebar main"    "footer footer";}.header   {  grid-area: header;}.sidebar   {  grid-area: sidebar;}

CSS Subgrid: Align Nested Grids Without Duplicating Tracks

Newly available
Old way
.parent   {  display: grid;  grid-template-columns: 1fr 1fr 1fr;}.child-grid   {  display: grid;  grid-template-columns: 1fr 1fr 1fr;}
Modern
.parent   {  display: grid;  grid-template-columns: 1fr 1fr 1fr;}.child-grid   {  display: grid;  grid-template-columns: subgrid;}

CSS gap Property for Flex and Grid Spacing

Widely available
Old way
.grid   {  display: flex;}.grid > *   {  margin-right: 16px;}.grid > *:last-child   {  margin-right: 0;}
Modern
.grid   {  display: flex;  gap: 16px;}

CSS aspect-ratio Property (No Padding Hack)

Widely available
Old way
.video-wrapper   {  position: relative;  padding-top: 56.25%;}.video-wrapper > *   {  position: absolute;  top: 0;  left: 0;  width: 100%;  height: 100%;}
Modern
.video-wrapper   {  aspect-ratio: 16 / 9;}

CSS Sticky Header with position: sticky

Widely available
Old way
// JavaScript: scroll listenerwindow.addEventListener('scroll', () => {  const rect = header.getBoundingClientRect();  if (rect.top <= 0)    header.classList.add('fixed');  else    header.classList.remove('fixed');});.header.fixed {  position: fixed;  top: 0;}
Modern
.header   {  position: sticky;  top: 0;}

Responsive Components with CSS Container Queries

Widely available
Old way
.card   {  display: grid;  grid-template-columns: 1fr;}@media (min-width: 768px)   {  .card   {    grid-template-columns: auto 1fr;  }}
Modern
.wrapper   {  container-type: inline-size;}.card   {  grid-template-columns: 1fr;}@container (width > 400px)   {  .card   {    grid-template-columns: auto 1fr;  }}

Center a Div in CSS with place-items: center

Widely available
Old way
.parent   {  position: relative;}.child   {  position: absolute;  top: 50%;  left: 50%;  transform: translate(-50%, -50%);}
Modern
.parent   {  display: grid;  place-items: center;}

CSS Floating Labels with :placeholder-shown

Widely available
Old way
/* .filled class toggled by JS on every keystroke */.field.filled label,.field input:focus + label   {  translate: 0 -1.4rem;  font-size: .75rem;}// JS: toggle .filled on input input.addEventListener('input', () => {   input.closest('.field').classList.toggle('filled', input.value.length > 0); });
Modern
input:not(:placeholder-shown) + label,input:focus + label   {  translate: 0 -1.4rem;  font-size: .75rem;}/* no .filled class, no JS */

CSS :playing, :paused, :muted Pseudo-Classes for Media

Limited availability
Old way
const video = document.querySelector('video');const wrap = video.closest('.player');video.addEventListener('play', () => wrap.classList.add('is-playing'));video.addEventListener('pause', () => wrap.classList.remove('is-playing'));video.addEventListener('waiting', () => wrap.classList.add('is-buffering'));video.addEventListener('playing', () => wrap.classList.remove('is-buffering'));video.addEventListener('volumechange', () => {  wrap.classList.toggle('is-muted', video.muted);});
Modern
video:paused + .controls .play-icon { display: block; }video:playing + .controls .pause-icon { display: block; }video:buffering + .controls .spinner { display: block; }video:muted + .controls .muted-badge { display: block; }.player:has(video:playing) {  outline: 2px solid currentColor;}

CSS Custom Highlight API: ::highlight() Pseudo-Element

Newly available
Old way
function highlight(el, term)   {  el.innerHTML = el.innerHTML    .replace(       new RegExp(term, 'gi'),       '<mark>$&</mark>'     );}
Modern
/* CSS */::highlight(search)   {  background: yellow;}/* JS — no DOM changes */const range = new Range();range.setStart(node, startOffset);range.setEnd(node, endOffset);CSS.highlights.set(   'search', new Highlight(range));

CSS Form Validation Styles with :user-invalid

Newly available
Old way
/* only style after .touched class is added by JS */input.touched:invalid   {  border-color: red;}input.touched:valid   {  border-color: green;}// JS: add .touched class on blur input.addEventListener('blur', () => {   input.classList.add('touched'); });
Modern
input:user-invalid   {  border-color: red;}input:user-valid   {  border-color: green;}/* no JS, no .touched class */

CSS Scroll Spy with :target-current (No IntersectionObserver)

Limited availability
Old way
/* JS IntersectionObserver approach */const observer = new IntersectionObserver(   (entries) =>  {  entries.forEach(entry =>  {    const link = document         .querySelector(`a[href="# // [!code --]        ${entry.target.id}"]`);       link.classList.toggle(         'active',        entry.isIntersecting);  });},   {  threshold: 0.5});document.querySelectorAll('section')   .forEach(s =>    observer.observe(s));
Modern
.scroller   {  overflow-y: auto;}nav a:target-current   {  color: var(--accent);}

Low-Specificity CSS Reset with :where()

Widely available
Old way
ul, ol   {  margin: 0;  padding-left: 1.5rem;}/* Specificity (0,0,2). Component .list { padding: 0 } loses. */
Modern
:where(ul, ol)   {  margin: 0;  padding-inline-start: 1.5rem;}/* Specificity 0. .list { padding: 0 } wins. */

Group CSS Selectors with :is() and :where()

Widely available
Old way
.card h1, .card h2, .card h3, .card h4   {  margin-bottom: 0.5em;}// Same prefix repeated for every selector
Modern
.card :is(h1, h2, h3, h4)   {  margin-bottom: 0.5em;}

CSS Focus Styles with :focus-visible

Widely available
Old way
button:focus   {  outline: 2px solid blue;}// Outline appears on mouse click. Often removed with outline: none.
Modern
button:focus-visible   {  outline: 2px solid var(--focus-color);}

CSS :has() Selector: Select Parent by Child

Newly available
Old way
// Watch for changes, find parentdocument.querySelectorAll('input')  .forEach(input => {    input.addEventListener('invalid', () => {      input.closest('.form-group')        .classList.add('has-error');    });  });
Modern
.form-group:has(input:invalid)   {  border-color: red;  background: #fff0f0;}

CSS contrast-color() for Readable Text

Limited availability
Old way
/* Hardcode a text color per background */.badge-blue   {  background: #1e40af;  color: white;}.badge-yellow   {  background: #fde047;  color: black;}/* Repeat for every color variant */
Modern
.badge   {  background: var(--bg);  color: contrast-color(var(--bg));}

OKLCH Color in CSS: Perceptually Uniform Colors

Widely available
Old way
/* HSL: same L value, different perceived brightness */--yellow: hsl(60 100% 50%);/* blinding */--blue:   hsl(240 100% 50%);/* dark *//* manually tweak each shade to look balanced */--brand-light: hsl(246 87% 68%);
Modern
/* oklch: L is perceptually uniform across hues */--brand:       oklch(0.55 0.2 264);--brand-light: oklch(0.75 0.2 264);--brand-dark:  oklch(0.35 0.2 264);/* only L changes */

Frosted Glass Effect in CSS with backdrop-filter

Widely available
Old way
.card::before   {  content: '';  position: absolute;  inset: 0;  background-image: url(bg.jpg);  background-size: cover;  filter: blur(12px);  z-index: -1;}
Modern
.glass   {  backdrop-filter: blur(12px) saturate(1.5);  background: rgba(255,255,255,.1);}

CSS Wide-Gamut Color: display-p3 and rec2020

Newly available
Old way
.hero   {  color: #c85032;}/* Hex, rgb(), hsl() are sRGB. Limited on wide-gamut screens. */
Modern
.hero   {  color: oklch(0.7 0.25 29);}/* Or: color(display-p3 1 0.2 0.1) for P3. */

CSS Relative Color Syntax: Lighten, Darken Without Sass

Newly available
Old way
/* Sass: @use 'sass:color'; *//* Sass: color.adjust($brand, $lightness: 20%) */.btn   {  background: lighten(var(--brand), 20%);}
Modern
.btn   {  background:    oklch(from var(--brand) calc(l + 0.2) c h);}

Dark Mode Colors in CSS with light-dark()

Newly available
Old way
:root   {  --text: #111;  --bg: #fff;}@media (prefers-color-scheme: dark)   {  :root   {    --text: #eee;    --bg: #222;  }}
Modern
:root   {  color-scheme: light dark;  color: light-dark(#111, #eee);}

CSS accent-color for Checkboxes, Radios, and Range Inputs

Widely available
Old way
input[type="checkbox"]   {  appearance: none;  width: 1.25rem;  height: 1.25rem;  border: 2px solid ...;  background: ...;  border-radius: ...;}
Modern
input[type="checkbox"],input[type="radio"]   {  accent-color: #7c3aed;}

CSS color-mix() Function (No Sass Required)

Newly available
Old way
// _variables.scss$blue: #3b82f6;$pink: #ec4899;$blend: mix($blue, $pink, 60%);
Modern
.card   {  background: color-mix(     in oklch, #3b82f6 60%, #ec4899   );}

CSS Feature Detection with @supports (No Modernizr)

Widely available
Old way
// JS: detect support, add classif (CSS.supports('container-type', 'inline-size')) {  document.documentElement.classList.add('has-container-queries');}
Modern
@supports (display: grid)   {  .layout   {    display: grid;  }}

CSS Range Style Queries: style(--progress > 50%)

Limited availability
Old way
/* Multiple discrete checks */@container style(--progress: 0%)   {  .bar   {    background: red;  }}@container style(--progress: 25%)   {  .bar   {    background: orange;  }}@container style(--progress: 50%)   {  .bar   {    background: yellow;  }}/* ... repeat for every threshold *//* Or use JavaScript to set classes *//* based on the numeric value */
Modern
@container style( /* [!code ++] */--progress > 75% /* */)   {  .bar   {    background: var(--green);  }}

CSS attr() with Type: Typed HTML Attribute Values

Limited availability
Old way
/* JS: read data attr and set style */document.querySelectorAll('.bar')   .forEach(el =>  {  const pct = el.dataset.pct;     el.style.width = pct + '%';     el.style.setProperty(       '--pct', pct     );});
Modern
.bar   {  width: attr(     data-pct type(<percentage>)   );}

CSS if() Function for Inline Conditional Styles

Limited availability
Old way
/* Multiple class variants */.btn   {  background: gray;}.btn.primary   {  background: blue;}.btn.danger   {  background: red;}/* Plus JS to manage state */el.classList.toggle(   'primary',  isPrimary );/* Duplicated rules per variant */
Modern
.btn   {  background: if(     style(--variant: primary):      blue;  else: gray   );}

CSS @function for Reusable Logic (No Sass Mixins)

Limited availability
Old way
// Sass / SCSS@function fluid($min, $max) {  @return clamp( /* [!code --] */    $min,    calc($min + ($max - $min) * ((100vw - 320px) / 960)),    $max  ); /* [!code --] */}
Modern
@function --fluid( /* */--min, --max /* */)   {  @return clamp( /* [!code ++] */  var(--min), /* [!code ++] */  50vi, /* [!code ++] */  var(--max) /* [!code ++] */  );}

CSS Lazy Rendering with content-visibility: auto

Newly available
Old way
// JavaScriptconst observer = new IntersectionObserver(  (entries) => {    entries.forEach(entry => {      if (entry.isIntersecting) {        renderContent(entry.target);      }    });  });
Modern
.section   {  content-visibility: auto;  contain-intrinsic-size: auto 500px;}

CSS @scope: Scoped Styles Without BEM

Newly available
Old way
.card__title   {  font-size: 1.25rem;  margin-bottom: 0.5rem;}.card__body   {  color: #444;}/* HTML: class="card__title" */
Modern
@scope (.card)   {  .title   {    font-size: 1.25rem;    margin-bottom: 0.5rem;  }  .body   {    color: #444;  }}/* HTML: class="card", class="title" */

CSS @property for Typed Custom Properties

Newly available
Old way
:root   {  --hue: 0;}.wheel   {  background: hsl(var(--hue), 80%, 50%);  transition: --hue .3s;  /* ignored, not interpolable */}
Modern
@property --hue   {  syntax: "<angle>";  inherits: false;  initial-value: 0deg;}.wheel   {  background: hsl(var(--hue), 80%, 50%);  transition: --hue .3s;}

CSS Dark Mode with color-scheme: light dark

Widely available
Old way
@media (prefers-color-scheme: dark)   {  input, select, textarea, button   {    background: #333;    color: #eee;  }  ::-webkit-scrollbar   {    ...  }}
Modern
:root   {  color-scheme: light dark;}

Control CSS Specificity with @layer (No !important)

Widely available
Old way
.card .title   {  font-size: 1rem;}.page .card .title   {  font-size: 1.25rem;}.page .card .title.special   {  color: red !important;}// More selectors or !important to win
Modern
@layer base, components, utilities;@layer utilities   {  .mt-4   {    margin-top: 1rem;  }}

CSS Custom Properties for Theme Variables

Widely available
Old way
// Sass/LESS: compile-time only$primary: #7c3aed;$spacing: 16px;.btn {  background: $primary;  padding: $spacing;}
Modern
:root   {  --primary: #7c3aed;  --spacing: 16px;}.btn   {  background: var(--primary);}

Native CSS Nesting (No Sass or Less)

Newly available
Old way
// nav.scss, requires Sass compiler.nav {  display: flex;  gap: 8px;  & a {    color: blue;  }}
Modern
/* nav.css, plain CSS, no compiler */.nav   {  display: flex;  gap: 8px;  & a   {    color: #888;    text-decoration: none;    &:hover   {      color: white;    }  }}

Vertically Center Text in CSS with text-box-trim

Limited availability
Old way
.btn   {  display: inline-flex;  align-items: center;  padding: 10px 20px;  padding-top: 8px;}
Modern
.btn   {  padding: 10px 20px;  text-box: trim-both cap alphabetic;}

Truncate Text to N Lines in CSS with line-clamp

Widely available
Old way
/* JS: slice text, append '...'. *//* or measure via getComputedStyle. Breaks on resize. */.card-title   {  overflow: hidden;  text-overflow: ellipsis;}/* Older: -webkit-line-clamp + -webkit-box-orient */
Modern
.card-title   {  display: -webkit-box;  -webkit-line-clamp: 3;  line-clamp: 3;  overflow: hidden;}/* Standard; -webkit- prefix still needed. */

CSS Drop Caps with the initial-letter Property

Limited availability
Old way
.drop-cap::first-letter   {  float: left;  font-size: 3em;  line-height: 1;  margin-right: 8px;}
Modern
.drop-cap::first-letter   {  -webkit-initial-letter: 3;  initial-letter: 3;}/* Standard; -webkit- prefix needed for Safari. */

Balance Headlines in CSS with text-wrap: balance

Newly available
Old way
/* HTML: manual <br> or wrapper for JS */h1   {  text-align: center;  max-width: 40ch;  /* Balance-Text.js: script + init */  /* or insert <br> in CMS/HTML */}
Modern
h1, h2   {  text-wrap: balance;  max-width: 40ch;}

font-display: swap in CSS (Prevent Invisible Text)

Widely available
Old way
@font-face   {  font-family: "MyFont";  src: url("MyFont.woff2");}
Modern
@font-face   {  font-family: "MyFont";  src: url("MyFont.woff2");  font-display: swap;}

Variable Fonts in CSS: One File, All Weights

Widely available
Old way
@font-face   {  font-family: "MyFont";  src: url("MyFont-Regular.woff2");  font-weight: 400;}@font-face   {  font-weight: 700;  ...}@font-face   {  font-weight: 600;  ...}
Modern
@font-face   {  font-family: "MyVar";  src: url("MyVar.woff2");  font-weight: 100 900;}

Fluid Typography in CSS with clamp()

Widely available
Old way
h1   {  font-size: 1rem;}@media (min-width: 600px)   {  h1   {    font-size: 1.5rem;  }}@media (min-width: 900px)   {  h1   {    font-size: 2rem;  }}
Modern
h1   {  font-size: clamp(1rem, 2.5vw, 2rem);}

Also on this site

ESC