CSS is a language for describing the rendering of document with a DOM. Selectors are a core component of CSS, and can be used in JS too, to locate DOM nodes.

The basic builing blocks are the seven types of simple selectors: Universal selector, Type selector, Id selector, Class selector, Attribute selector, Pseudo-classes, Pseudo-elements. *, form, #login, .error, [rel='nofollow'], :first-child, ::before are all examples of simple selectors. They can be used as they are, but often they are not specific enough. Most of the simple selectors have been around forever and are well established; most of the Level 4 work has been in adding or expanding to pseudo-classes.

Simple selectors can be combined in a sequence (without white spaces) to create compound selectors, which are often more specific. All the simple selectors that make up a compound selector must match, in any order. The only rule is that there can only be a single Universal or Type selector, and if present it must be the first. *:first-child, input.error.important, #login[data-attempts='3'], a[rel='nofollow']::before are examples of compound selectors.

Simple or compound selectors can be combined with Combinators to create complex selectors. Combinators are operators that tell the engine something about DOM nodes around the one you are selecting, which is always the last one in the selector. So in a > *:first-child the combinator > is used to combine the simple selector a with the compound selector *:first-child (which is made up of the simple selectors * and :first-child). The subject of the selector is *:first-child; the other part, a > is not being selected, it’s just context, so CSS rules won’t be applied to it.

Finally, simple, compound, or complex selectors can be combined in selector lists using the list operator ,. A selector list will match if any selector within it matches. a:hover, a:visited is an example of a selector list.

To summarise:

  • a is a simple selector that will match all links
  • a:hover is a compound selector that will match all elements which are links AND are being hovered on
  • a :hover (note the space) is a complex selector that will match all elements which are being hovered on, but only if one of their ancestors is a link (probably a mistake)
  • a, :hover is a selector list that will match either a link OR any element being hovered on10px

    Universal selector, *


    Nothing new here. This selector matches everything* and it is only necessary for when that’s exactly whay you need. Typical example is setting the box-sizing property:
* { box-sizing: border-box; }
* strictly speaking the universal selector only matches element which are not _featurless_, which is the case for some shadow-dom elements.

Otherwise it’s redundant. All the pairs below do exactly the same thing

*[rel='nofollow'] { .. }
[rel='nofollow'] { .. }

*.error { ... }
.error { ... }

*:first-child { ... }
:first-child { ... }

However, it may make sense to use it anyway, for added clarity. For example below one may miss out the space between nav and :first-child when scanning the code quickly. The * makes it obvious

nav :first-child { ... }
nav *:first-child { ... }

Type selector, i.e. element name

This is basically the name of a tag or element - _input_, _textarea_, _a_, _component_, _nav_ ... there is nothing new or interesting about these, which exist since the beginning of CSS. They are case insensitive.
a { ... }
li { ... }

Class selectors

Another simple at exists since the beginning of time. Nothing new happening here either; they remain case-sensitive (unless the document is in quirks mode, which luckily is now pretty much a historical curiosity). This could be replaced with the attribute selector for whitespace-separated lists
.error { ... }
[class~=error] { ... }

.error.severe { ... }
[class~=error][class~=severe] { ... }

Id selectors

Yep. Same old simple selector, nothing new here either. Of course, nobody uses them anymore, right?
#myId { ... }
[id='myId'] { ... }

Attribute selectors

A powerful family of simple selectors which also has been around for a while.

You can select only elements which have a certain attribute, regardless of value

[rel] { .. }

Or you can treat the value as a single string, and match either the complete string, or parts of it, trating spaces as just another character.

[rel='nofollow'] { .. }

This will match any element with a rel attribute which is exactly 'nofollow'. This will not match rel="nofollow noreferrer"

[rel='nofollow noreferrer'] { .. }

This _will_ match "nofollow noreferrer", but not "noreferrer nofollow", which is why it is never a good idea to have selectors with spaces in them (unless we are talking strings in data attributes, like "new york" and so on)

[rel*='nofollow][rel*='noreferrer'] { .. }

This compound selector is a safer way to find elements with two values; this will handle both rel="noreferrer nofollow" and rel="nofollow noreferrer". But the `~=` operator (see below) is better, as it was designed for that purpose

a[href^='http:'] { .. }

`^=` is the 'starts with` selector. This matches all links if their href href attribute starts with 'http:', i.e. all links to non-https sites

a[href$='index.html'] { border: 10px solid red; }

`$=` is the companion of the previous one, the 'ends with' selector. You can use this to visually mark all the links which have the pointless "index.html" at the end

a[href*='amazon'] { .. }

And finally `*=`, the 'anywhere' selector; matches all links with an href which includes 'amazon' anywhere (including beginning or end).

The attribute selectors above treat the value of the attribute as a single string. There are two operators that treat it as a sequence of patterns

a[rel~='nofollow`] { ... }

The `~=` operator treats the strings as a whitespace separated list, and tries to match the string against every item in the list. The above will match "nofollow", "noreferrer nofollow" and "nofollow noreferrer"

/* these two are equivalent */
title[lang|='fr'] { .. }

title[lang^='fr-'], title[lang='fr']  { .. }

The `|=` operator was created mostly to match locales and languages. It either treats the string as a complete match (`fr`), or as the beginning of a dash separated sequence (`fr-fr`, `fr-be` etc). in theory it shouldn't be actually very useful for language matching, because it only matches element with an actual lang attribute. For safe language matching, the `:lang` pseudo-class was created. But that's not fully supported. Having said that, I am yet to see elements with actual different lang attributes on the same document in a real life situation. **TL;DR pretty useless**

New: case insensitive attribute values

[data-location='Paris']   { .. }  /* case sensitive version */
[data-location='paris' i] { .. }  /* case insensitive version */

All the matchers are case-sensitive by default, but there is a case-insensitive operator that can be added to the selector. [Compatibility](https://caniuse.com/#feat=css-case-insensitive): at the time of writing no IE present or future :-(

Pseudo-classes

Pseudo-classes give information about DOM elements which is not contained in the DOM tree, or is hard to obtain. They start with `:`, and can be simple attributes or functions that take arguments. They are beasically a random collection of useful CSS stuff.

A lot of changes in Level 4 for these simple selectors.

Updated: not()

`:not(selector-list)` used to take a single simple selector, but it can now take a whole selector list. Lovely idea, but so far only Safari seems interested... ([Compatibility](https://caniuse.com/#feat=css-not-sel-list))

:not(form *) { all selectors that are not inside a form }
button:not([disabled]) { all non disabled buttons }
**Dirty hack** incidentally, something like `div:not(span)` is exactly the same as `div`, but has more specifity, so it would "win" the specificity war, should that be needed

New: matches()

`:matches(selector-list)` was called :any, (which explains [the MDN entry for it](https://developer.mozilla.org/en-US/docs/Web/CSS/:any)), so support is a bit confusing ([Compatibility](https://caniuse.com/#feat=css-matches-pseudo))

It is supposed to do some of the work that SASS and the like do with nesting.

:matches(article, section, div, aside, nav) h1 { ... }

/* instead of: */
article h1,
section h1,
aside h1,
nav h1 { ... }

New: has()

A lot of developers have been crying out for this - FINALLY WE CAN CELEBR… oh wait. So far only MS are considering putting it into a future version of Edge. Maybe. The others are not even touching it with a bargepole. The Compatibility page links to the Mozilla issue dealing with it. It’s from 10 years ago and is currently unassigned…

For what it's worth, here is what it _would_ do, if someone bothered to actually implement it:

a:has(> img) { all links with image tags inside them }
section:not(:has(h1, h2)) { all sections with no h1 or h2 inside them}

Classic pseudo-classes

:link and :visited, those old warhorses, remain unchanged. As does :target, a most useful selector for SPAs

Minor pseudo-classes

There are a lot of pseudo-classes in the specs that are not going to be implemented any time soon.

:any-link is basically a shortcut for both :link and :visited, and it’s supported with vendor tags on all browsers except for IE. That, and the fact the documentation has a note “Any better name suggestions for this pseudo?” shows it’s better to leave alone for now.

:lang is superior to the |= attribute selector, because the latter only matches elements which have an actual lang attribute, whereas :lang is supposed to be able to work out an element’s lang from its parent. But it’s so badly supported it doesn’t even get a caniuse page

The :dir pseudo-class, like :lang, matches element with a given dir whether they have an attribute for it or not. All well and good, but hardly anyone’s implemented it yet - Compatibility

:current, :past, :future (to do with voice readers), :drop, :drop(), :focus-ring are others pseudoselector that are not going to be implemented any time soon.