Stop writing CSS like it's 2015.

Modern CSS snippets next to the old hacks they replace. Every technique you still look up has a native replacement now.

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

All comparisons

85 snippets

Video player states without JavaScript events

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

Text highlighting without DOM manipulation

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

Path shapes without SVG clip paths

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

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

Data-driven layouts without JavaScript charts

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

Automatic type scales from document structure

Limited availability
Old
h1   {  font-size: 2.5rem;}h2   {  font-size: 2rem;}h3   {  font-size: 1.5rem;}h4   {  font-size: 1.25rem;}h5   {  font-size: 1.1rem;}h6   {  font-size: 1rem;}
Modern
/* Experimental: requires :heading(), sibling-index(), and pow() support */:heading   {  font-weight: 600;  font-size:    calc(1rem * pow(1.2, 5 - sibling-index()));}/* Fallback: keep a simple manual ladder for all other browsers */h1   {  font-size: 2.5rem;}h2   {  font-size: 2rem;}h3   {  font-size: 1.5rem;}

Scaling elements without transform hacks

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

Readable text without manual contrast checks

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

Perceptually uniform colors with oklch

Widely available
Old
/* 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 without opacity hacks

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

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

Better mobile keyboards without JavaScript

Widely available
Old
/* Hack: use type=tel to get numeric keyboard */<input type="tel" pattern="[0-9]*" />/* No way to control the return key label */if (/Mobi/.test(navigator.userAgent))   {  submitBtn.textContent = 'Search';}
Modern
<!-- Numeric pad, no type=tel hack --><input type="text" inputmode="numeric" /><!-- Search keyboard with Search return key --><input type="search" inputmode="search" enterkeyhint="search" />

Live form output without DOM writes

Widely available
Old
<input type="range" id="volume" /><div id="display"></div> const input = document.getElementById('volume');const display = document.getElementById('display');input.addEventListener('input', () =>  {  display.textContent = input.value;});
Modern
<input type="range" id="volume" name="volume" /><output for="volume">50</output>

Lazy load images without JavaScript

Widely available
Old
<!-- HTML: data-src instead of src --><img data-src="photo.jpg" class="lazy" /> const observer = new IntersectionObserver(([entry]) =>  {  if (entry.isIntersecting)  {    entry.target.src = entry.target.dataset.src;     observer.unobserve(entry.target);  }});document.querySelectorAll('.lazy').forEach(img => observer.observe(img));
Modern
<!-- Above the fold: no lazy --><img src="hero.jpg" alt="Hero" /><!-- Below the fold: lazy load --><img src="product.jpg" alt="Product" loading="lazy" />

Accordion disclosure without JavaScript

Widely available
Old
<!-- HTML: custom markup --><button aria-expanded="false" aria-controls="panel">  What is CSS?</button><div id="panel" hidden>  <p>CSS is a stylesheet language.</p></div>
Modern
<!-- No JS, no aria wiring --><details>  <summary>What is CSS?</summary>  <p>CSS is a stylesheet language.</p></details>

Native autocomplete without JavaScript

Limited availability
Old
/* Add autocomplete library (Awesomplete, Typeahead, etc.) */import Awesomplete from 'awesomplete';new Awesomplete(document.querySelector('#country'),  {  list: ['Afghanistan', 'Albania', 'Algeria', ...]});/* Plus custom CSS for the dropdown */
Modern
<input list="countries" id="country" name="country" /><datalist id="countries">  <option value="United States" />  <option value="Canada" />  <option value="Mexico" /></datalist>

Preventing layout shift from scrollbar appearance

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

Preventing scroll chaining without JavaScript

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

Scrollbar styling without -webkit- pseudo-elements

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

Mobile viewport height without the 100vh hack

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

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

CSS feature detection without JavaScript

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

Exclusive accordions without JavaScript

Widely available
Old
// Close all on toggle, reopen the clicked onedocument.querySelectorAll('details')  .forEach(d => {    d.addEventListener('toggle', () => {      if (d.open)        others.forEach(o => o.open = false);    });  });
Modern
<details name="faq">  <summary>Question 1</summary>  <p>Answer 1</p></details><details name="faq">  <summary>Question 2</summary>  <p>Answer 2</p></details>

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

Media query ranges without min-width and max-width

Widely available
Old
@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 without the background-image hack

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

Form validation styles without JavaScript

Newly available
Old
/* 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 */

Auto-growing textarea without JavaScript

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

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

Range style queries without multiple blocks

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

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

Typed attribute values without JavaScript

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

Inline conditional styles without JavaScript

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

Reusable CSS logic without Sass mixins

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

Corner shapes beyond rounded borders

Limited availability
Old
.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;}

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

Scroll spy without IntersectionObserver

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

Filling available space without calc workarounds

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

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

Carousel navigation without a JavaScript library

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

Vertical text centering without padding hacks

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

Hover tooltips without JavaScript events

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

Modal controls without onclick handlers

Limited availability
Old
<!-- Inline onclick or JS event listener --><button onclick="  document.querySelector('#dlg') // [!code --]  .showModal()"> // [!code --]  Open Dialog</button><dialog id="dlg">...</dialog>
Modern
<button commandfor="dlg" command="show-modal">  Open Dialog</button><dialog id="dlg">...</dialog>

Dialog light dismiss without click-outside listeners

Limited availability
Old
/* JS: detect click outside dialog bounds */dialog.addEventListener('click', (e) =>  {  const rect = dialog.getBoundingClientRect();   if (e.clientX < rect.left ||      e.clientX > rect.right ||      e.clientY < rect.top ||      e.clientY > rect.bottom)  {    dialog.close();  }});
Modern
<dialog closedby="any">  Click outside or press ESC to close</dialog>

Customizable selects without a JavaScript library

Limited availability
Old
/* JS: replace native select with custom DOM */import Choices from 'choices.js';new Choices('#my-select',   {  searchEnabled: false,});/* Plus ~30 lines of custom CSS for   .choices__inner, .choices__list, etc. */
Modern
select,select ::picker(select)   {  appearance: base-select;}select option:checked   {  background: var(--accent);}

Vivid colors beyond sRGB

Newly available
Old
.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. */

Color variants without Sass functions

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

Multiline text truncation without JavaScript

Widely available
Old
/* 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. */

Drop caps without float hacks

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

Positioning shorthand without four properties

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

Lazy rendering without IntersectionObserver

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

Dropdown menus without JavaScript toggles

Newly available
Old
.menu   {  display: none;}.menu.open   {  display: block;}/* JS: toggle class, document click-outside,   keydown ESC, aria-expanded, aria-hidden */
Modern
/* <button popovertarget="menu"> ... <div popover> */#menu[popover]   {  position: absolute;  margin: 0.25rem 0;}/* Toggle, light dismiss, ESC: built in. */

Tooltip positioning without JavaScript

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

Scoped styles without BEM naming

Newly available
Old
.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" */

Typed custom properties without JavaScript

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

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 snapping without a carousel library

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

Balanced headlines without manual line breaks

Newly available
Old
/* 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 loading without invisible text

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

Multiple font weights without multiple files

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

Dark mode defaults without extra CSS

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

Dark mode colors without duplicating values

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

Low-specificity resets without complicated selectors

Widely available
Old
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. */

Direction-aware layouts without left and right

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

Naming grid areas without line numbers

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

Aligning nested grids without duplicating tracks

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

Modal dialogs without a JavaScript library

Widely available
Old
.overlay   {  position: fixed;  inset: 0;  background: rgb(0 0 0 / .5);}/* JS: addEventListener click, keydown ESC, focus trap,   aria-hidden, body scroll lock, z-index stacking */
Modern
dialog   {  padding: 1rem;}dialog::backdrop   {  background: rgb(0 0 0 / .5);}/* JS: dialog.showModal(); dialog.close(); */

Styling form controls without rebuilding them

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

Grouping selectors without repetition

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

Focus styles without annoying mouse users

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

Controlling specificity without !important

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

Theme variables without a preprocessor

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

Fluid typography without media queries

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

Spacing elements without margin hacks

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

Aspect ratios without the padding hack

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

Sticky headers without JavaScript scroll listeners

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

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

Nesting selectors without Sass or Less

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

Responsive components without media queries

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

Mixing colors without a preprocessor

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

Selecting parent elements without JavaScript

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

Centering elements without the transform hack

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

Frequently asked

What is modern-css.com?

A reference site for web developers showing side-by-side comparisons of outdated CSS techniques and their modern native replacements. It covers 70+ CSS properties and features including Grid, custom properties, CSS nesting, container queries, scroll-driven animations, anchor positioning, and more.

Is modern-css.com the same as moderncss.dev?

No. They are different sites. This site (modern-css.com) is a snippets and reference project with side-by-side old vs modern comparisons. It is built by me (Naeem). moderncss.dev is Stephanie Eckles’s Modern CSS Solutions article series: separate site, author, and content.

Do I still need CSS preprocessors like Sass or Less?

Many features once exclusive to CSS preprocessors are now built into native CSS. CSS custom properties replace Sass variables, native CSS nesting replaces Sass nesting syntax, color-mix() and relative color syntax replace Sass color functions, and @layer replaces manual specificity management. Most common Sass use cases can now be handled with native CSS.

What browsers support modern CSS features?

Browser support varies by feature. Core properties like CSS Grid, Custom Properties, Flexbox gap, and aspect-ratio are widely available with 95%+ global support. Newer features like CSS nesting, container queries, :has(), and anchor positioning are newly available across all major browsers. Each snippet shows a Baseline status and a live browser support percentage sourced from caniuse.

What is CSS Baseline?

CSS Baseline is a classification system from the Web Platform project that describes how broadly a web feature is supported. Widely available means all major browsers have supported it for at least 2.5 years. Newly available means it recently shipped in all major browsers. Limited availability means it works in some browsers but not all. Every snippet on this site shows its Baseline status.

Is this accordion built with JavaScript?

No. This FAQ uses the native HTML <details> and <summary> elements. The browser handles open and close natively — no JavaScript required. The + toggle indicator is a CSS ::after pseudo-element. A fitting way to answer questions about modern web features.

ESC