CSS selectors allow you to choose elements by type, attributes, or location within the HTML document. This tutorial explains three new options — :is(), :where(), and :has().
Selectors are commonly used in stylesheets. The following example locates all
paragraph elements and changes the font weight to bold:
p {
font-weight: bold;
}
You can also use selectors in JavaScript to locate DOM nodes:
Pseudo-class selectors target HTML elements based on their current state. Perhaps the most well known is :hover
, which applies styles when the cursor moves over an element, so it’s used to highlight clickable links and buttons. Other popular options include:
:visited
: matches visited links:target
: matches an element targeted by a document URL:first-child
: targets the first child element:nth-child
: selects specific child elements:empty
: matches an element with no content or child elements:checked
: matches a toggled-on checkbox or radio button:blank
: styles an empty input field:enabled
: matches an enabled input field:disabled
: matches a disabled input field:required
: targets a required input field:valid
: matches a valid input field:invalid
: matches an invalid input field:playing
: targets a playing audio or video element
Browsers have recently received three more pseudo-class selectors…
The CSS :is Pseudo-class Selector
Note: this was originally specified as :matches()
and :any()
, but :is()
has become the CSS standard.
You often need to apply the same styling to more than one element. For example,
paragraph text is black by default, but gray when it appears within an
, or
:
p {
color: #000;
}
article p,
section p,
aside p {
color: #444;
}
This is a simple example, but more sophisticated pages will lead to more complicated and verbose selector strings. A syntax error in any selector could break styling for all elements.
CSS preprocessors such as Sass permit nesting (which is also coming to native CSS):
article, section, aside {
p {
color: #444;
}
}
This creates identical CSS code, reduces typing effort, and can prevent errors. But:
- Until native nesting arrives, you’ll need a CSS build tool. You may want to use an option like Sass, but that can introduce complications for some development teams.
- Nesting can cause other problems. It’s easy to construct deeply nested selectors that become increasingly difficult to read and output verbose CSS.
:is()
provides a native CSS solution which has full support in all modern browsers (not IE):
:is(article, section, aside) p {
color: #444;
}
A single selector can contain any number of :is()
pseudo-classes. For example, the following complex selector applies a green text color to all
,
, and
elements that are children of a
which has a class of .primary
or .secondary
and which isn’t the first child of an
:
article section:not(:first-child):is(.primary, .secondary) :is(h1, h2, p) {
color: green;
}
article section:not(:first-child):is(.primary, .secondary) :is(h1, h2, p) {
color: green;
}
The equivalent code without :is()
required six CSS selectors:
article section.primary:not(:first-child) h1,
article section.primary:not(:first-child) h2,
article section.primary:not(:first-child) p,
article section.secondary:not(:first-child) h1,
article section.secondary:not(:first-child) h2,
article section.secondary:not(:first-child) p {
color: green;
}
Note that :is()
can’t match ::before
and ::after
pseudo-elements, so this example code will fail:
div:is(::before, ::after) {
display: block;
content: '';
width: 1em;
height: 1em;
color: blue;
}
The CSS :where Pseudo-class Selector
:where()
selector syntax is identical to :is()
and is also supported in all modern browsers (not IE). It will often result in identical styling. For example:
:where(article, section, aside) p {
color: #444;
}
The difference is specificity. Specificity is the algorithm used to determine which CSS selector should override all others. In the following example, article p
is more specific than p
alone, so all paragraph elements within an
article p { color: #444; }
p { color: #000; }
In the case of :is()
, the specificity is the most specific selector found within its arguments. In the case of :where()
, the specificity is zero.
Consider the following CSS:
article p {
color: black;
}
:is(article, section, aside) p {
color: red;
}
:where(article, section, aside) p {
color: blue;
}
Let’s apply this CSS to the following HTML:
<article>
<p>paragraph textp>
article>
The paragraph text will be colored red, as shown in the following CodePen demo.
See the Pen
Using the :is selector by SitePoint (@SitePoint)
on CodePen.
The :is()
selector has the same specificity as article p
, but it comes later in the stylesheet, so the text becomes red. It’s necessary to remove both the article p
and :is()
selectors to apply a blue color, because the :where()
selector is less specific than either.
More codebases will use :is()
than :where()
. However, the zero specificity of :where()
could be practical for CSS resets, which apply a baseline of standard styles when no specific styling is available. Typically, resets apply a default font, color, paddings and margins.
This CSS reset code applies a top margin of 1em
to
headings unless they’re the first child of an
element:
h2 {
margin-block-start: 1em;
}
article :first-child {
margin-block-start: 0;
}
h2 {
margin-block-start: 1em;
}
article :first-child {
margin-block-start: 0;
}
Attempting to set a custom
top margin later in the stylesheet has no effect, because article :first-child
has a higher specificity:
h2 {
margin-block-start: 2em;
}
h2 {
margin-block-start: 2em;
}
You can fix this using a higher-specificity selector, but it’s more code and not necessarily obvious to other developers. You’ll eventually forget why you required it:
article h2:first-child {
margin-block-start: 2em;
}
You can also fix the problem by applying !important
to each style, but please avoid doing that! It makes further styling and development considerably more challenging:
h2 {
margin-block-start: 2em !important;
}
A better choice is to adopt the zero specificity of :where()
in your CSS reset:
:where(h2) {
margin-block-start: 1em;
}
:where(article :first-child) {
margin-block-start: 0;
}
You can now override any CSS reset style regardless of the specificity; there’s no need for further selectors or !important
:
h2 {
margin-block-start: 2em;
}
The CSS :has Pseudo-class Selector
The :has()
selector uses a similar syntax to :is()
and :where()
, but it targets an element which contains a set of others. For example, here’s the CSS for adding a blue, two-pixel border to any link that contains one or more
or
a:has(img, section) {
border: 2px solid blue;
}
This is the most exciting CSS development in decades! Developers finally have a way to target parent elements!
The elusive “parent selector” has been one of the most requested CSS features, but it raises performance complications for browser vendors, and therefor has been a long time coming. In simplistic terms:
- Browsers apply CSS styles to an element when it’s drawn on the page. The whole parent element must therefore be re-drawn when adding further child elements.
- Adding, removing, or modifying elements in JavaScript could affect the styling of the whole page right up to the enclosing
.
Assuming the vendors have resolved performance issues, the introduction of :has()
permits possibilities that would have been impossible without JavaScript in the past. For example, you can set the styles of an outer form