Selectors comparisons
9 old vs modern selectors CSS techniques, side by side.
Browser compatibility:
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;}