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.
.parent { position: relative;}.child { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);} .parent { display: grid; place-items: center;} All comparisons
85 snippetsVideo player states without JavaScript events
Limited availabilityconst 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);}); 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 availablefunction highlight(el, term) { el.innerHTML = el.innerHTML .replace( new RegExp(term, 'gi'), '<mark>$&</mark>' );} /* 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/* 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 */} .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// 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);} .card { rotate: random(-15deg, 15deg); animation-delay: random(0s, 1s);} Data-driven layouts without JavaScript charts
Widely available.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);}); .bar { block-size: 1.5rem; background: var(--accent); inline-size: attr(data-pct percentage);} Automatic type scales from document structure
Limited availabilityh1 { 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;} /* 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.thumbnail { transform: scale(0.5); transform-origin: top left; margin-bottom: -50%; margin-right: -50%;} .thumbnail { zoom: 0.5;} Readable text without manual contrast checks
Limited availability/* Hardcode a text color per background */.badge-blue { background: #1e40af; color: white;}.badge-yellow { background: #fde047; color: black;}/* Repeat for every color variant */ .badge { background: var(--bg); color: contrast-color(var(--bg));} Perceptually uniform colors with oklch
Widely available/* 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%); /* 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.card::before { content: ''; position: absolute; inset: 0; background-image: url(bg.jpg); background-size: cover; filter: blur(12px); z-index: -1;} .glass { backdrop-filter: blur(12px) saturate(1.5); background: rgba(255,255,255,.1);} Motion path animation without JavaScript
Widely available<!-- 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 }}); @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/* 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';} <!-- 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<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;}); <input type="range" id="volume" name="volume" /><output for="volume">50</output> Lazy load images without JavaScript
Widely available<!-- 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)); <!-- 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<!-- 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> <!-- 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/* 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 */ <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/* 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 */ body { scrollbar-gutter: stable;} Preventing scroll chaining without JavaScript
Widely available.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 .modal-content { overflow-y: auto; overscroll-behavior: contain;} Scrollbar styling without -webkit- pseudo-elements
Newly available/* 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;} * { scrollbar-width: thin; scrollbar-color: #888 transparent;} Mobile viewport height without the 100vh hack
Widely available.hero { height: 100vh;} .hero { height: 100dvh;} Reduced motion without JavaScript detection
Widely available// 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' );} @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; }} CSS feature detection without JavaScript
Widely available// JS: detect support, add classif (CSS.supports('container-type', 'inline-size')) { document.documentElement.classList.add('has-container-queries');} @supports (display: grid) { .layout { display: grid; }} Exclusive accordions without JavaScript
Widely available// Close all on toggle, reopen the clicked onedocument.querySelectorAll('details') .forEach(d => { d.addEventListener('toggle', () => { if (d.open) others.forEach(o => o.open = false); }); }); <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// JS animation library for bounce/spring easinganime({ targets: el, translateX: 300, easing: 'spring(1, 80, 10, 0)'}); .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@media (min-width: 600px) and (max-width: 1200px) { .card { grid-template-columns: 1fr 1fr; }} @media (600px <= width <= 1200px) { .card { grid-template-columns: 1fr 1fr; }} Responsive images without the background-image hack
Widely available<!-- 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;} <img src="photo.jpg" alt="..." loading="lazy">img { object-fit: cover; width: 100%; height: 200px;} Form validation styles without JavaScript
Newly available/* 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'); }); input:user-invalid { border-color: red;}input:user-valid { border-color: green;}/* no JS, no .touched class */ Auto-growing textarea without JavaScript
Limited availabilitytextarea { overflow: hidden;}// JS: reset height then set to scrollHeight on every inputel.addEventListener('input', () => { el.style.height = 'auto'; el.style.height = el.scrollHeight + 'px'; }); textarea { field-sizing: content; min-height: 3lh;} Smooth height auto animations without JavaScript
Newly available.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 }); } :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/* 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 */ @container style( /* [!code ++] */--progress > 75% /* */) { .bar { background: var(--green); }} Sticky & snapped element styling without JavaScript
Limited availability/* JS scroll listener approach */const header = document .querySelector('.header');const offset = header .offsetTop;window.addEventListener( 'scroll', () => { header.classList .toggle('stuck', window.scrollY >= offset);}); .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/* 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 );}); .bar { width: attr( data-pct type(<percentage>) );} Inline conditional styles without JavaScript
Limited availability/* 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 */ .btn { background: if( style(--variant: primary): blue; else: gray );} Reusable CSS logic without Sass mixins
Limited availability// Sass / SCSS@function fluid($min, $max) { @return clamp( /* [!code --] */ $min, calc($min + ($max - $min) * ((100vw - 320px) / 960)), $max ); /* [!code --] */} @function --fluid( /* */--min, --max /* */) { @return clamp( /* [!code ++] */ var(--min), /* [!code ++] */ 50vi, /* [!code ++] */ var(--max) /* [!code ++] */ );} Corner shapes beyond rounded borders
Limited availability.card { clip-path: polygon( 0% 10%, 2% 4%, 4% 2%, 10% 0%, /* ...16 more points */ );}/* Or use an SVG mask image */ .card { border-radius: 2em; corner-shape: squircle;} Responsive clip paths without SVG
Limited availability.shape { clip-path: path( 'M0 200 L100 0 L200 200 Z' );} .shape { clip-path: shape( from 0% 100%, line to 50% 0%, line to 100% 100% );} Scroll spy without IntersectionObserver
Limited availability/* 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)); .scroller { overflow-y: auto;}nav a:target-current { color: var(--accent);} Filling available space without calc workarounds
Limited availability.full { width: 100%; width: calc(100% - 40px);} .full { width: stretch;} Staggered animations without nth-child hacks
Limited availability/* 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));} li { transition: opacity .25s ease, translate .25s ease; transition-delay: calc(0.1s * (sibling-index() - 1));} Carousel navigation without a JavaScript library
Limited availability/* 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 */ .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.btn { display: inline-flex; align-items: center; padding: 10px 20px; padding-top: 8px;} .btn { padding: 10px 20px; text-box: trim-both cap alphabetic;} Hover tooltips without JavaScript events
Limited availability/* 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', /* … */); <button interestfor="tip"> Hover me</button><div id="tip" popover=hint> Tooltip text</div> Modal controls without onclick handlers
Limited availability<!-- Inline onclick or JS event listener --><button onclick=" document.querySelector('#dlg') // [!code --] .showModal()"> // [!code --] Open Dialog</button><dialog id="dlg">...</dialog> <button commandfor="dlg" command="show-modal"> Open Dialog</button><dialog id="dlg">...</dialog> Dialog light dismiss without click-outside listeners
Limited availability/* 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(); }}); <dialog closedby="any"> Click outside or press ESC to close</dialog> Customizable selects without a JavaScript library
Limited availability/* 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. */ select,select ::picker(select) { appearance: base-select;}select option:checked { background: var(--accent);} Vivid colors beyond sRGB
Newly available.hero { color: #c85032;}/* Hex, rgb(), hsl() are sRGB. Limited on wide-gamut screens. */ .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/* Sass: @use 'sass:color'; *//* Sass: color.adjust($brand, $lightness: 20%) */.btn { background: lighten(var(--brand), 20%);} .btn { background: oklch(from var(--brand) calc(l + 0.2) c h);} Multiline text truncation without JavaScript
Widely available/* 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 */ .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.drop-cap::first-letter { float: left; font-size: 3em; line-height: 1; margin-right: 8px;} .drop-cap::first-letter { -webkit-initial-letter: 3; initial-letter: 3;}/* Standard; -webkit- prefix needed for Safari. */ Positioning shorthand without four properties
Widely available.overlay { position: absolute; top: 0; right: 0; bottom: 0; left: 0;} .overlay { position: absolute; inset: 0;} Lazy rendering without IntersectionObserver
Newly available// JavaScriptconst observer = new IntersectionObserver( (entries) => { entries.forEach(entry => { if (entry.isIntersecting) { renderContent(entry.target); } }); }); .section { content-visibility: auto; contain-intrinsic-size: auto 500px;} Dropdown menus without JavaScript toggles
Newly available.menu { display: none;}.menu.open { display: block;}/* JS: toggle class, document click-outside, keydown ESC, aria-expanded, aria-hidden */ /* <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/* JS: getBoundingClientRect for trigger + tooltip, compute top/left, handle scroll/resize */.tooltip { position: fixed; top: var(--computed-top); left: var(--computed-left);} .trigger { anchor-name: --tip;}.tooltip { position-anchor: --tip; top: anchor(bottom);} Scoped styles without BEM naming
Newly available.card__title { font-size: 1.25rem; margin-bottom: 0.5rem;}.card__body { color: #444;}/* HTML: class="card__title" */ @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:root { --hue: 0;}.wheel { background: hsl(var(--hue), 80%, 50%); transition: --hue .3s; /* ignored, not interpolable */} @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.icon { transform: translateX(10px) rotate(45deg) scale(1.2);}.icon:hover { transform: translateX(10px) rotate(90deg) scale(1.2); .icon { translate: 10px 0; rotate: 45deg; scale: 1.2;}.icon:hover { rotate: 90deg;} Animating display none without workarounds
Newly available.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'; }); .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.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'); });}); .card { transition: opacity .3s, transform .3s; @starting-style { opacity: 0; transform: translateY(10px); }} Page transitions without a framework
Newly available// 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 }) }]}); // 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// carousel.js + Slick/Swiperimport Swiper from 'swiper';new Swiper('.carousel', { slidesPerView: 1, loop: true}); .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/* 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 */} h1, h2 { text-wrap: balance; max-width: 40ch;} Font loading without invisible text
Widely available@font-face { font-family: "MyFont"; src: url("MyFont.woff2");} @font-face { font-family: "MyFont"; src: url("MyFont.woff2"); font-display: swap;} Multiple font weights without multiple files
Widely available@font-face { font-family: "MyFont"; src: url("MyFont-Regular.woff2"); font-weight: 400;}@font-face { font-weight: 700; ...}@font-face { font-weight: 600; ...} @font-face { font-family: "MyVar"; src: url("MyVar.woff2"); font-weight: 100 900;} Dark mode defaults without extra CSS
Widely available@media (prefers-color-scheme: dark) { input, select, textarea, button { background: #333; color: #eee; } ::-webkit-scrollbar { ... }} :root { color-scheme: light dark;} Dark mode colors without duplicating values
Newly available:root { --text: #111; --bg: #fff;}@media (prefers-color-scheme: dark) { :root { --text: #eee; --bg: #222; }} :root { color-scheme: light dark; color: light-dark(#111, #eee);} Low-specificity resets without complicated selectors
Widely availableul, ol { margin: 0; padding-left: 1.5rem;}/* Specificity (0,0,2). Component .list { padding: 0 } loses. */ :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.box { margin-left: 1rem; padding-right: 1rem;}[dir="rtl"] .box { margin-left: 0; margin-right: 1rem;} .box { margin-inline-start: 1rem; padding-inline-end: 1rem; border-block-start: 1px solid;} Naming grid areas without line numbers
Widely available.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;} .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.parent { display: grid; grid-template-columns: 1fr 1fr 1fr;}.child-grid { display: grid; grid-template-columns: 1fr 1fr 1fr;} .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.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 */ dialog { padding: 1rem;}dialog::backdrop { background: rgb(0 0 0 / .5);}/* JS: dialog.showModal(); dialog.close(); */ Styling form controls without rebuilding them
Widely availableinput[type="checkbox"] { appearance: none; width: 1.25rem; height: 1.25rem; border: 2px solid ...; background: ...; border-radius: ...;} input[type="checkbox"],input[type="radio"] { accent-color: #7c3aed;} Grouping selectors without repetition
Widely available.card h1, .card h2, .card h3, .card h4 { margin-bottom: 0.5em;}// Same prefix repeated for every selector .card :is(h1, h2, h3, h4) { margin-bottom: 0.5em;} Focus styles without annoying mouse users
Widely availablebutton:focus { outline: 2px solid blue;}// Outline appears on mouse click. Often removed with outline: none. button:focus-visible { outline: 2px solid var(--focus-color);} Controlling specificity without !important
Widely available.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 @layer base, components, utilities;@layer utilities { .mt-4 { margin-top: 1rem; }} Theme variables without a preprocessor
Widely available// Sass/LESS: compile-time only$primary: #7c3aed;$spacing: 16px;.btn { background: $primary; padding: $spacing;} :root { --primary: #7c3aed; --spacing: 16px;}.btn { background: var(--primary);} Fluid typography without media queries
Widely availableh1 { font-size: 1rem;}@media (min-width: 600px) { h1 { font-size: 1.5rem; }}@media (min-width: 900px) { h1 { font-size: 2rem; }} h1 { font-size: clamp(1rem, 2.5vw, 2rem);} Spacing elements without margin hacks
Widely available.grid { display: flex;}.grid > * { margin-right: 16px;}.grid > *:last-child { margin-right: 0;} .grid { display: flex; gap: 16px;} Aspect ratios without the padding hack
Widely available.video-wrapper { position: relative; padding-top: 56.25%;}.video-wrapper > * { position: absolute; top: 0; left: 0; width: 100%; height: 100%;} .video-wrapper { aspect-ratio: 16 / 9;} Sticky headers without JavaScript scroll listeners
Widely available// 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;} .header { position: sticky; top: 0;} Scroll-linked animations without a library
Limited availability// 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)); @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// nav.scss, requires Sass compiler.nav { display: flex; gap: 8px; & a { color: blue; }} /* 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.card { display: grid; grid-template-columns: 1fr;}@media (min-width: 768px) { .card { grid-template-columns: auto 1fr; }} .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// _variables.scss$blue: #3b82f6;$pink: #ec4899;$blend: mix($blue, $pink, 60%); .card { background: color-mix( in oklch, #3b82f6 60%, #ec4899 );} Selecting parent elements without JavaScript
Newly available// Watch for changes, find parentdocument.querySelectorAll('input') .forEach(input => { input.addEventListener('invalid', () => { input.closest('.form-group') .classList.add('has-error'); }); }); .form-group:has(input:invalid) { border-color: red; background: #fff0f0;} Centering elements without the transform hack
Widely available.parent { position: relative;}.child { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);} .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.