Modern CSS snippets. 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 way
.parent   {  position: relative;}.child   {  position: absolute;  top: 50%;  left: 50%;  transform: translate(-50%, -50%);}
Modern
.parent   {  display: grid;  place-items: center;}

Recently added

85 snippets

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

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

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

Automatic CSS Type Scale with sibling-index() and pow()

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

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

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

HTML inputmode and enterkeyhint for Mobile Keyboards

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

HTML output Element for Live Form Output

Widely available
Old way
<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 in HTML with loading="lazy"

Widely available
Old way
<!-- 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" />

CSS Accordion with details and summary (No JavaScript)

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

HTML datalist: Native Autocomplete (No JavaScript)

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

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

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 Exclusive Accordion with the details name Attribute

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

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

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 */

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

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

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

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

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

Fill Available Space in CSS with the stretch Keyword

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

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

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

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 Modal Controls with commandfor and command Attributes

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

CSS Dialog Light Dismiss with closedby Attribute

Limited availability
Old way
/* 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 CSS Select with appearance: base-select

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

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

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. */

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 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-Only Dropdown Menu with the Popover API

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

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 @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 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) */

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

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

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

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

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. */

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 Modal Dialog with the Native dialog Element

Widely available
Old way
.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(); */

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

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

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

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

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

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

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

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

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

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

Frequently asked

What is modern.css?

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 the same as moderncss.dev?

No. They are different sites. This site (modern.css) 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