Updated for 2026

Stop writing CSS
like it's 2015.

Modern CSS code snippets, side by side with the old hacks they replace. Every technique you still Google has a clean, native replacement now.

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

/* child needs nothing. */

All comparisons

75 snippets
Browser compatibility:
Selectors
Advanced

Text highlighting without DOM manipulation

Old el.innerHTML = el.innerHTML
  .replace(/term/g, '<mark>$&</mark>');
Modern ::highlight(search) {
  background: yellow;
}
see modern →
Layout
Advanced

Path shapes without SVG clip paths

Old clip-path: path("M 0 0 L 800 0...");
/* px values — not responsive */
Modern clip-path: shape(
  from 0% 0%, line to 100% 0%,
  curve to 0% 80% via 50% 105%);
see modern →
Layout
Beginner

Scaling elements without transform hacks

Old transform: scale(0.5);
margin-bottom: -50%; /* hack */
Modern .thumb { zoom: 0.5; }
see modern →
Colors
Beginner

Readable text without manual contrast checks

Old /* Pick white or black manually */
color: white; /* hardcoded */
Modern color: contrast-color(var(--bg));
see modern →
Animation
Beginner

Reduced motion without JavaScript detection

Old const mq = window.matchMedia('(prefers-reduced-motion)');
if (mq.matches) el.style.animation = 'none';
Modern @media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.01ms !important; }
}
see modern →
Color
Intermediate

Perceptually uniform colors with oklch

Old --brand: #4f46e5;
--brand-light: #818cf8;
--brand-dark: #3730a3;
/* guess-and-check each shade */
Modern --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, same perceived hue */
see modern →
Workflow
Beginner

CSS feature detection without JavaScript

Old // Modernizr or manual check
if (CSS.supports('display', 'grid')) {
  el.classList.add('grid');
}
Modern @supports (display: grid) {
  .layout { display: grid; }
}
/* no JS required */
see modern →
Color
Intermediate

Frosted glass effect without opacity hacks

Old .card::before { content: '';
  background-image: url(bg.jpg);
  filter: blur(12px);
  z-index: -1; }
Modern .glass {
  backdrop-filter: blur(12px);
  background: rgba(255,255,255,.1);
}
see modern →
Layout
Beginner

Exclusive accordions without JavaScript

Old // JS: close all, open clicked
details.forEach(d => d.open = false);
this.open = true;
Modern <details name="faq">...</details>
<details name="faq">...</details>
/* browser closes others */
see modern →
Animation
Intermediate

Custom easing curves without cubic-bezier guessing

Old /* JS animation library */
anime({ targets: el,
  easing: 'easeOutBounce' });
Modern transition: transform 1s
  linear(0, 1.2, 0.9, 1.05, 1);
/* bounce — no JS needed */
see modern →
Layout
Beginner

Preventing layout shift from scrollbar appearance

Old body { overflow-y: scroll; }
/* or hardcode the scrollbar width */
body { padding-right: 17px; }
Modern body {
  scrollbar-gutter: stable;
}
/* scrollbar space always reserved */
see modern →
Layout
Beginner

Media query ranges without min-width and max-width

Old @media (min-width: 600px)
         and (max-width: 1200px) {
  /* styles */
}
Modern @media (600px <= width <= 1200px) {
  /* styles */
}
see modern →
Layout
Beginner

Preventing scroll chaining without JavaScript

Old // JS: block page scroll when inside modal
modal.addEventListener('wheel', e =>
  e.preventDefault(), { passive: false })
Modern .modal-content {
  overflow-y: auto;
  overscroll-behavior: contain;
}
/* page stays still */
see modern →
Layout
Beginner

Responsive images without the background-image hack

Old .card-image {
  background-image: url(...);
  background-size: cover;
  background-position: center;
}
Modern img {
  object-fit: cover;
  width: 100%;
  height: 200px;
}
see modern →
Layout
Beginner

Scrollbar styling without -webkit- pseudo-elements

Old /* webkit only */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-thumb { background: #888; }
Modern * {
  scrollbar-width: thin;
  scrollbar-color: #888 transparent;
}
see modern →
Layout
Beginner

Mobile viewport height without the 100vh hack

Old .hero {
  height: 100vh;
}
/* overflows on mobile */
Modern .hero {
  height: 100dvh;
}
/* adapts to browser chrome */
see modern →
Selector
Beginner

Form validation styles without JavaScript

Old // JS: add .touched on blur
el.addEventListener('blur', () =>
  el.classList.add('touched'))
/* .touched:invalid { color: red } */
Modern input:user-invalid {
  border-color: red;
}
input:user-valid {
  border-color: green;
}
see modern →
Layout
Beginner

Auto-growing textarea without JavaScript

Old // JS: resize on every keystroke
el.addEventListener('input', () => {
  el.style.height = 'auto';
  el.style.height = el.scrollHeight + 'px'; })
Modern textarea {
  field-sizing: content;
  min-height: 3lh;
}
/* grows with content, no JS */
see modern →
Animation
Beginner

Smooth height auto animations without JavaScript

Old // measure, set px, then snap to auto
el.style.height = el.scrollHeight + 'px';
el.addEventListener('transitionend', ...)
Modern :root { interpolate-size: allow-keywords; }
.accordion { height: 0; overflow: hidden;
  transition: height .3s ease; }
.accordion.open { height: auto; }
see modern →
Workflow
Advanced

Range style queries without multiple blocks

Old /* Multiple style() blocks */
@container style(--p: 51%) {}
@container style(--p: 52%) {}
/* ...for each value */
Modern @container style(
  --progress > 50%
) {
  .bar { ... }
}
see modern →
Animation
Intermediate

Sticky & snapped element styling without JavaScript

Old window.addEventListener(
  'scroll', () => {
    /* check position */
});
Modern @container scroll-state(
  stuck: top
) {
  .header { ... }
}
see modern →
Workflow
Intermediate

Typed attribute values without JavaScript

Old // JS reading dataset
el.style.width =
  el.dataset.pct + '%';
Modern .bar {
  width: attr(
    data-pct type(<percentage>)
  );
}
see modern →
Workflow
Intermediate

Inline conditional styles without JavaScript

Old // JavaScript toggling
el.classList.toggle(
  'primary', isPrimary
);
Modern .btn {
  background: if(
    style(--variant: primary):
      blue; else: gray
  );
}
see modern →
Workflow
Intermediate

Reusable CSS logic without Sass mixins

Old // Sass function
@function fluid($min, $max) {
  @return clamp(...);
}
Modern @function --fluid(
  --min, --max
) {
  @return clamp(...);
}
see modern →
Layout
Beginner

Corner shapes beyond rounded borders

Old .card {
  clip-path: polygon(
    ... /* 20+ points */
  );
}
Modern .card {
  border-radius: 2em;
  corner-shape: squircle;
}
see modern →
Animation
Advanced

Responsive clip paths without SVG

Old .shape {
  clip-path: path(
    'M0 200 L100 0...'
  );
}
Modern .shape {
  clip-path: shape(
    from 0% 100%, ...
  );
}
see modern →
Selector
Intermediate

Scroll spy without IntersectionObserver

Old const observer = new
  IntersectionObserver(cb);
/* 15+ lines of JS */
Modern nav a:target-current {
  color: var(--accent);
}
see modern →
Layout
Beginner

Filling available space without calc workarounds

Old .full {
  width: calc(100% - 40px);
  /* or width: 100% and overflow */
}
Modern .full {
  width: stretch;
}
/* fills container, keeps margins */
see modern →
Animation
Intermediate

Staggered animations without nth-child hacks

Old li:nth-child(1) { --i: 0; }
li:nth-child(2) { --i: 1; }
li:nth-child(3) { --i: 2; }
/* repeat for every item… */
Modern li {
  transition-delay:
    calc(0.1s * (sibling-index() - 1));
}
see modern →
Layout
Advanced

Carousel navigation without a JavaScript library

Old // Swiper.js or Slick carousel
new Swiper('.carousel', {
  navigation: { /* … */ },
  pagination: { /* … */ },
});
Modern .carousel::scroll-button(right) {
  content: "➡";
}
.carousel li::scroll-marker {
  content: '';
}
see modern →
Typography
Beginner

Vertical text centering without padding hacks

Old .btn {
  padding: 10px 20px;
  /* looks off-center, tweak top/bottom */
  padding-top: 8px; /* hack */
}
Modern h1, button {
  text-box: trim-both cap alphabetic;
}
/* true optical centering */
see modern →
Layout
Intermediate

Hover tooltips without JavaScript events

Old // JS: mouseenter + mouseleave
btn.addEventListener('mouseenter',
  () => showTooltip())
/* + focus, blur, positioning */
Modern <button interestfor="tip">Hover me</button>
<div id="tip" popover=hint>
  Tooltip content
</div>
see modern →
Layout
Beginner

Modal controls without onclick handlers

Old <button onclick="
  document.querySelector('#dlg')
  .showModal()">Open</button>
Modern <button commandfor="dlg"
  command="show-modal">Open</button>
<dialog id="dlg">...</dialog>
see modern →
Layout
Beginner

Dialog light dismiss without click-outside listeners

Old // JS: listen for click on ::backdrop
dialog.addEventListener('click',
  (e) => { /* check bounds */ })
Modern <dialog closedby="any">
  Click outside to close
</dialog>
/* no JS listeners */
see modern →
Layout
Intermediate

Customizable selects without a JavaScript library

Old // Select2 or Choices.js
new Choices('#my-select');
/* rebuilds entire DOM */
Modern select,
select ::picker(select) {
  appearance: base-select;
}
see modern →
Color
Intermediate

Vivid colors beyond sRGB

Old .hero {
  color: rgb(200, 80, 50);
}
/* sRGB only, washed on P3 */
Modern .hero {
  color: oklch(0.7 0.25 29);
}
/* or color(display-p3 1 0.2 0.1) */
see modern →
Color
Advanced

Color variants without Sass functions

Old /* Sass: lighten($brand, 20%), darken($brand, 10%) */
.btn { background: #e0e0e0; }
Modern .btn {
  background: oklch(from var(--brand) calc(l + 0.2) c h);
}
see modern →
Typography
Beginner

Multiline text truncation without JavaScript

Old /* JS: slice text by chars/words, add "..." */
.card-title { overflow: hidden; }
Modern .card-title {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  line-clamp: 3;
}
see modern →
Typography
Beginner

Drop caps without float hacks

Old .drop-cap::first-letter {
  float: left;
  font-size: 3em; line-height: 1;
}
Modern .drop-cap::first-letter {
  -webkit-initial-letter: 3;
  initial-letter: 3;
}
see modern →
Layout
Beginner

Positioning shorthand without four properties

Old .overlay {
  top: 0; right: 0;
  bottom: 0; left: 0;
}
Modern .overlay {
  position: absolute;
  inset: 0;
}
see modern →
Workflow
Intermediate

Lazy rendering without IntersectionObserver

Old // JS IntersectionObserver
new IntersectionObserver(
  (entries) => { /* render */ }
).observe(el);
Modern .section {
  content-visibility: auto;
  contain-intrinsic-size: auto 500px;
}
see modern →
Layout
Beginner

Dropdown menus without JavaScript toggles

Old .menu { display: none; }
.menu.open { display: block; }
/* + JS: click, clickOutside, ESC, aria */
Modern button[popovertarget=menu] { }
#menu[popover] {
  position: absolute;
}
see modern →
Layout
Advanced

Tooltip positioning without JavaScript

Old /* Popper.js / Floating UI: compute rect,
position: fixed, update on scroll */
.tooltip { position: fixed; }
Modern .trigger { anchor-name: --tip; }
.tooltip {
  position-anchor: --tip;
  top: anchor(bottom);
}
see modern →
Workflow
Advanced

Scoped styles without BEM naming

Old // BEM: .card__title, .card__body
.card__title { … }
.card__body { … }
// or CSS Modules / styled-components */
Modern @scope (.card) {
  .title { font-size: 1.25rem; }
  .body { color: #444; }
}
/* .title only inside .card */
see modern →
Workflow
Advanced

Typed custom properties without JavaScript

Old // --hue was a string, no animation
:root { --hue: 0; }
hsl(var(--hue), …) /* no interpolation */
Modern @property --hue {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}
/* animatable, validated */
see modern →
Animation
Beginner

Independent transforms without the shorthand

Old .icon { transform: translateX(10px) rotate(45deg) scale(1.2); }
/* change one = rewrite all */
Modern .icon {
  translate: 10px 0;
  rotate: 45deg;
  scale: 1.2;
}
/* animate any one without touching the rest */
see modern →
Animation
Intermediate

Animating display none without workarounds

Old // wait for transitionend then display:none
el.addEventListener('transitionend', …)
visibility + opacity + pointer-events
Modern .panel { transition: opacity .2s, overlay .2s;
  transition-behavior: allow-discrete; }
.panel.hidden { opacity: 0; display: none; }
/* no JS wait or visibility hack */
see modern →
Animation
Intermediate

Entry animations without JavaScript timing

Old // add class after paint
requestAnimationFrame(() => {
  el.classList.add('visible');
});
Modern .card { transition: opacity .3s, transform .3s; }
.card { @starting-style { opacity: 0; transform: translateY(10px); } }
/* no rAF/setTimeout */
see modern →
Animation
Advanced

Page transitions without a framework

Old // Barba.js or React Transition Group
Barba.init({ … })
transition hooks + duration state
Modern document.startViewTransition(() => updateDOM());
.hero { view-transition-name: hero; }
/* no Barba, no React TG */
see modern →
Layout
Intermediate

Scroll snapping without a carousel library

Old // Slick, Swiper, or scroll/touch JS
$('.carousel').slick({ … })
touchstart / scroll handlers
Modern .carousel { scroll-snap-type: x mandatory; }
.carousel > * { scroll-snap-align: start; }
/* no lib, no touch handlers */
see modern →
Typography
Beginner

Balanced headlines without manual line breaks

Old // manual <br> or Balance-Text.js
h1 { text-align: center; }
.balance-text /* JS lib */
Modern h1, h2 {
  text-wrap: balance;
}
/* no br or JS */
see modern →
Typography
Beginner

Font loading without invisible text

Old @font-face { ... }
/* Default: invisible text until load */
Modern @font-face {
  font-family: "MyFont";
  font-display: swap;
}
see modern →
Typography
Intermediate

Multiple font weights without multiple files

Old @font-face { font-weight: 400; }
@font-face { font-weight: 700; }
/* 4+ files */
Modern @font-face {
  font-family: "MyVar";
  src: url("MyVar.woff2");
  font-weight: 100 900;
}
see modern →
Workflow
Beginner

Dark mode defaults without extra CSS

Old @media (prefers-color-scheme: dark) {
  input, select, textarea { ... }
}
Modern :root {
  color-scheme: light dark;
}
see modern →
Color
Intermediate

Dark mode colors without duplicating values

Old @media (prefers-color-scheme: dark) {
  color: #eee;
}
Modern color: light-dark(#111, #eee);
color-scheme: light dark;
see modern →
Selector
Intermediate

Low-specificity resets without complicated selectors

Old .reset ul, .reset ol { ... }
/* or (0,0,1) specificity, still wins */
Modern :where(ul, ol) {
  margin: 0;
  padding-inline-start: 1.5rem;
}
see modern →
Layout
Intermediate

Direction-aware layouts without left and right

Old margin-left: 1rem;
padding-right: 1rem;
[dir="rtl"] .box { margin-right: ... }
Modern margin-inline-start: 1rem;
padding-inline-end: 1rem;
border-block-start: 1px solid;
see modern →
Layout
Beginner

Naming grid areas without line numbers

Old float: left; /* clearfix, margins */
grid-column: 1 / 3;
grid-row: 2;
Modern .layout {
  display: grid;
  grid-template-areas: "header header" "sidebar main" "footer footer";
}
see modern →
Layout
Advanced

Aligning nested grids without duplicating tracks

Old .child-grid {
  grid-template-columns: 1fr 1fr 1fr;
/* duplicate parent tracks */
}
Modern .child-grid {
  display: grid;
  grid-template-columns: subgrid;
}
see modern →
Layout
Intermediate

Modal dialogs without a JavaScript library

Old .overlay { position: fixed; z-index: 999; }
/* + JS: open/close, ESC, focus trap */
Modern dialog {
  padding: 1rem;
}
dialog::backdrop { background: rgb(0 0 0 / .5); }
see modern →
Color
Beginner

Styling form controls without rebuilding them

Old appearance: none;
// + 20+ lines of custom box/border/background
Modern input[type="checkbox"],
input[type="radio"] {
  accent-color: #7c3aed;
}
see modern →
Selector
Beginner

Grouping selectors without repetition

Old .card h1, .card h2, .card h3, .card h4 {
  margin-bottom: 0.5em;
}
Modern .card :is(h1, h2, h3, h4) {
  margin-bottom: 0.5em;
}
see modern →
Selector
Beginner

Focus styles without annoying mouse users

Old :focus { outline: 2px solid blue; }
// Shows on mouse click too, or people remove it (a11y fail)
Modern :focus-visible {
  outline: 2px solid var(--focus-color);
}
see modern →
Workflow
Intermediate

Controlling specificity without !important

Old .card .title { ... }
.page .card .title { ... }
.page .card .title.special { color: red !important; }
Modern @layer base, components, utilities;
@layer utilities { .mt-4 { margin-top: 1rem; } }
see modern →
Workflow
Beginner

Theme variables without a preprocessor

Old // Sass: $primary: #7c3aed;
// Compiles to static #7c3aed
.btn { background: $primary; }
Modern :root {
  --primary: #7c3aed;
}
.btn { background: var(--primary); }
see modern →
Typography
Intermediate

Fluid typography without media queries

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);
}
see modern →
Layout
Beginner

Spacing elements without margin hacks

Old .grid > * { margin-right: 16px; }
.grid > *:last-child { margin-right: 0; }
Modern .grid {
  display: flex;
  gap: 16px;
}
see modern →
Layout
Beginner

Aspect ratios without the padding hack

Old .wrapper { padding-top: 56.25%; position: relative; }
.inner { position: absolute; inset: 0; }
Modern .video-wrapper {
  aspect-ratio: 16 / 9;
}
see modern →
Layout
Beginner

Sticky headers without JavaScript scroll listeners

Old // JS: scroll listener + getBoundingClientRect
// then add/remove .fixed class
.header.fixed { position: fixed; }
Modern .header {
  position: sticky;
  top: 0;
}
see modern →
Animation
Advanced

Scroll-linked animations without a library

Old // JS + IntersectionObserver
observer.observe(el)
el.style.opacity = …
Modern animation-timeline: view();
animation-range: entry;
/* pure CSS, GPU-accelerated */
see modern →
Workflow
Beginner

Nesting selectors without Sass or Less

Old // requires Sass compiler
.nav {
  & a { color: #888; }
}
Modern .nav {
  & a { color: #888; }
}
/* plain .css, no build */
see modern →
Layout
Intermediate

Responsive components without media queries

Old @media (max-width: 768px) {
  .card { … }
}
/* viewport, not container */
Modern @container (width < 400px) {
  .card { flex-direction: column; }
}
see modern →
Colors
Intermediate

Mixing colors without a preprocessor

Old // Sass required
$blend: mix(
  $blue, $pink, 60%);
Modern background: color-mix(
  in oklch, #3b82f6,
  #ec4899);
see modern →
Selectors
Intermediate

Selecting parent elements without JavaScript

Old // JavaScript required
el.closest('.parent')
  .classList.add(…)
Modern .card:has(img) {
  grid-template: auto 1fr;
}
see modern →
Layout
Beginner

Centering elements without the transform hack

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

Your first month is free

Managed WordPress hosting for faster sites.

Learn more

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.

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.

New CSS drops.

Join 600+ readers who've survived clearfix hacks.

ESC