Selectors comparisons
11 old vs modern selectors CSS techniques, side by side.
Browser compatibility:
CSS @container style(): Variant Styles Without JavaScript
Newly availableOld way
.card--primary { background: oklch(0.55 0.2 260); color: #fff;}.card--danger { background: oklch(0.55 0.2 30); color: #fff;}// JS: el.querySelectorAll('.card').forEach(c => c.classList.add('card--' + el.dataset.variant));Modern
@property --variant { syntax: "<custom-ident>"; initial-value: default; inherits: true;}.card-wrap { container-name: card;}@container card style(--variant: primary) { .card { background: oklch(0.55 0.2 260); color: #fff; }}@container card style(--variant: danger) { .card { background: oklch(0.55 0.2 30); color: #fff; }}/* no JS, no class toggling */CSS :nth-child(n of selector): Target Nth Matching Element
Widely availableOld way
.item:nth-child(2n) { background: var(--accent); /* [!code --] */}/* counts ALL siblings — breaks when non-.featured items are mixed in */// JS: items.forEach((el, i) => { if (el.classList.contains('featured') && i % 2 === 0) el.classList.add('nth-even') });Modern
.item:nth-child(2n of .featured) { background: var(--accent);}/* counts only .featured items; other siblings don't affect the pattern */CSS Floating Labels with :placeholder-shown
Widely availableOld 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 availabilityOld 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 availableOld 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 availableOld 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 availabilityOld 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 availableOld 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 availableOld way
.card h1, .card h2, .card h3, .card h4 { margin-bottom: 0.5em;}// Same prefix repeated for every selectorModern
.card :is(h1, h2, h3, h4) { margin-bottom: 0.5em;}CSS Focus Styles with :focus-visible
Widely availableOld 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 availableOld 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;}