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 text-box-trim and text-box-edge for cap-height centering
Limited availability Old way
/* per-font line-height tuning to fake a centered look */.btn { line-height: 1.15; padding: 13px 20px 11px;} Modern
.btn { text-box-trim: trim-both; text-box-edge: cap alphabetic; padding: 12px 20px;}/* the button measures from the cap line, not the metric box */ 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 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);}