Complex CSS Selectors Inside Selectors

Since last year, WebKit supports many selectors defined in the draft of CSS Selectors Level 4.

In this post, we will see a special subset of new selectors: new pseudo classes that take other selectors as arguments. Those are :matches(), :not(), :nth-child(), and :nth-last-child().

All the selectors discussed in this post work for styling with CSS, and for querying with JavaScript (querySelector(), querySelectorAll(), closest(), etc).

Select Properties A or B

The pseudo class :matches() takes a list of selector as argument. It matches an element if any of the selector in the list matches.

For example, if we have:

:matches(.foo, .bar) {
    background-color: green;
}

Any element with the class “foo”, “bar”, or both will have a green background.

This new selector is used extensively inside WebKit itself to simplify stylesheets. For example, code like the following used to appear in Web Inspector:

.syntax-highlighted .css-keyword,
.syntax-highlighted .css-tag {
    color: rgb(170, 13, 145);
}

With :matches(), scoped selectors do not required any repetition:

.syntax-highlighted :matches(.css-keyword, .css-tag) {
    color: rgb(170, 13, 145);
}

Full Selector Support

The argument inside :matches() does not have to be a list of simple selectors. Any selector is supported, including the use of combinators.

For example, the following rule removes the top margin whenever two title levels follow each other:

:matches(h1+h2, h2+h3, h3+h4, h4+h5) {
    margin-top: 0;
}

There has been some confusion about the subject of the selector when using combinators with :matches(). For example, take the following rule:

.icon:matches(:any-link img) {}

It matches every image with a class “icon” and that has a link among its ancestors. It does not match links containing image elements. In fact, it is strictly equivalent to:

:any-link img.icon { }

If you are unsure what element :matches() would be applied to, you can just think of it as if the selector was invoking Element.matches() on the active element.

Deprecated :-webkit-any

WebKit already had a similar but more limited feature, the pseudo class :-webkit-any(). It is now deprecated.

The old :-webkit-any() has problems that cannot be fixed in a backward compatible way. It is best to avoid it. We removed all of its use inside WebKit.

Make sure to carefully test your pages when replacing :-webkit-any() by :matches() as they are not strictly equivalent. In particular, :-webkit-any() does not handle the specificity correctly.

Rejecting Complete Selectors

The pseudo class :not() has always been very popular in CSS. It selects an element if the element does not match a simple selector. For example:

a:not(.internal) {
    color: red;
}

What is new is the argument you can pass to :not(). Just like :matches(), the new :not() supports any selector list as arguments.

Code that used to chain multiple :not() can now use comma separated selectors to achieve the same. For example:

:not(i):not(em) { }

Can also be written as:

:not(i, em) { }

You can use complex selectors, which means you can exclude multiple properties. For example, it is now possible to select any element that does not have simultaneously the classes “.important” and “.dialog”:

:not(.important.dialog) { }

And you can use combinators which is becoming one of my favorite features. For example, it is possible to select any image that is not part of a link:

img:not(:any-link *) { }

Counting Elements

The pseudo class :nth-child() and :nth-last-child() are also being augmented with similar capabilities.

Previously, it was only possible to count elements indistinguishably with :nth-child(), or count elements having the same qualified name with :nth-of-type(). With the extended :nth-child(), you can specify what kind of element to count.

For example, if you want to select every other element that is not hidden, you can now do:

:nth-child(odd of :not([hidden])) { }

The general syntax is of the form:

:nth-child(An+B of selector list) { }

As for the previous selectors, the argument has full support for selectors, including complex selectors, combinators, etc.

Dynamic Specificity

Nesting selectors inside other selectors creates a new interesting question: what is the resulting specificity?

The answer is: it depends. The specificity varies with what is being matched. The final specificity is the largest of all the sub-selectors.

Let’s take a concrete example. If we take the following selector:

:matches(div, #foo, .bar) { }

What is the specificity?

When the following element is selected:

<div id="foo"></div>

The specificity is (1, 0, 0), the same as if we only had the selector “#foo”. The reason is for all the selector that match (“div” and “#foo”), the highest specificity of the two is the one of “#foo”.

This definition is great because it’s compatible with existing selectors. You can take any selector, wrap it inside a :matches(), and its specificity works exactly the same.

With complicated selectors, finding the specificity is not always this easy. Fortunately, the Web Inspector is there to help us.

In the Inspector, when hovering a selector in the Rules Style sidebar, the tooltip shows the real specificity of the selector for the current element. If the specificity varies with the element like in the example above, the Inspector also indicates that the specificity is dynamic:

The Web Inspector's tooltip show the dynamic specificity of any selector.

Final Word

There are many new possibilities created by the new selectors. Play with them, invent new combinations, and tell us what you think.

For short questions, you can contact me and Yusuke on Twitter. For longer questions, you can email webkit-help or file bug report.