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;}

Other categories

ESC