Selectors comparisons
8 old vs modern selectors CSS techniques, side by side.
Browser compatibility:
Video player states without JavaScript events
Limited availability Old
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;} Text highlighting without DOM manipulation
Newly available Old
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)); Form validation styles without JavaScript
Newly available Old
/* 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 */ Scroll spy without IntersectionObserver
Limited availability Old
/* 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 resets without complicated selectors
Widely available Old
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. */ Grouping selectors without repetition
Widely available Old
.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;} Focus styles without annoying mouse users
Widely available Old
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);} Selecting parent elements without JavaScript
Newly available Old
// 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;}