Last month in Sydney and Melbourne I was honoured to speak at Respond 2016, the third Web Directions conference at which I’ve had the privilege of speaking after Web Essentials 2006 and Web Directions South 2010. John Allsopp assigned me a great topic that would never have occurred to me otherwise: CSS selectors.

Here’s what I came up with:

title slide: CSS Selectors Redux. Kevin Yank, @sentience, kevin@cultureamp.com

I’m Kevin Yank. I work at Culture Amp, and today I’m here to talk about CSS selectors.

slide: CSS Selectors. A code listing showing a number of simple CSS declarations, with the part before the curly braces highlighted.
No prizes for spotting the selectors on this slide

They’re the things in pink. Selectors are the part of CSS that lets us point at particular bits of a web page and say “I want to style you.”

slide: what’s new; why there’s nothing new; how to be MacGyver
What we’ll be covering

In this talk, I’ll be covering what’s new in CSS selectors. I’ll then move swiftly along to why there’s nothing new in CSS selectors. And then we’ll finish strong with how to be MacGyver.

slide: what’s new (a non-exhaustive list)

Mostly, there are a bunch of new pseudo-classes in CSS:

slide: :enabled, :disabled, :checked
There are selectors for form field states
slide: :valid, :invalid, :required, :optional, :read-only, :read-write, :in-range, :out-of-range and :user-error (unsupported)
There are selectors for HTML5 form validations

Because most form fields start off empty and therefore invalid, most of these are of limited usefulness, unless you’re happy with your forms starting mostly red. The :user-error pseudo-class has been proposed to match fields that are invalid as a result of user input, but it doesn’t have any support yet.

slide: :first-child, :last-child, :only-child, :first-of-type, :last-of-type, :only-of-type, and highlighted: :nth-child(), :nth-last-child(), :nth-of-type() and :nth-last-of-type()
There are a whole bunch of selectors for selecting particular children inside a container

The last four are especially interesting, because they let us build really flexible selections of children:

slide: :nth-child(4) selects the 4th child
They let us select a particular child
slide: :nth-child(even) selects the 2nd, 4th, 6th child and so on
Just the even or odd children
slide: :nth-child(n+4) selects the 4th, 5th, 6th child and so on
The 4th child onward
slide: :nth-child(-n+4) selects the 1st, 2nd, 3rd and 4th child
Everything up to the 4th child
slide: :nth-child(3n+1) selects the 1st, 4th, 7th child and so on
Every 3rd child, starting with the first
slide: :nth-last-child(3) selects the 5th child out of 7
The third to last child
slide: :nth-last-child(-n+3) selects the 5th, 6th and 7th child out of 7
The last three children

These child-selecting pseudo-classes will come in handy in some surprising ways a bit later.

slide: :empty Example: .errors { border: 1px solid red; } .errors:empty { display: none; }
Style an element differently if it has no content

:empty can be handy if, say, your page contains a div into which your JavaScript might insert form validation errors, but if there are no errors, then you probably don’t want to display that div at all.

slide: :empty doesn’t apply to a div with a line break between the opening and closing tags
:empty can be finnicky

Unfortuntely, :empty won’t match an element if it contains so much as a just line break, so you really have to control the whitespace in your markup.

slide: :blank also matches elements that contain only whitespace
:blank has been proposed as a better :empty

There’s basically no support for :blank yet, but it’ll be handy if it catches on.

slide: :target doesn’t apply to a lightbox that is otherwise styled with display: none

The :target pseudo-class lets you style the element whose ID appears in the fragment identifier in the URL. You can use this to do surprising things like create lightboxes without any JavaScript! Here we have a hyperlink that points to the message element in the page, which is a lightbox that’s styled hidden by default.

slide: the lightbox appears after following the link, and contains a close button that links back to an empty fragment identifier

When you click the link, the URL points to the lightbox’s ID, so the :target selector matches it, and the new styles make the lightbox appear. The lightbox can also contain a link back to the empty fragment identifier, so that when you click it…

slide: the URL has no fragment identifier, so the lightbox is hidden

…the lightbox is hidden again. All without a shred of JavaScript!

slide: :not Example: article:not(:last-child) can be used to apply a border to the bottom of all but the last in a series of articles
:not lets you invert the meaning of simple selectors

Say you want to put a border on the bottom of all but the last in a series of articles. You might think to apply the border to all articles, and then with a second rule remove the border from the last article. Instead, with a single selector using :not, you can apply the border efficiently by selecting all but the last child.

slide: a[href]:not([class]) matches standard links only, .my_link_class can then match links with a class
These two selectors are mutually exclusive

In this example, the :not pseudo-class lets you match only links that have no class assigned. These might be hyperlinks in your body content, the ones that come out of your site’s content management system, for example. You can then use simple class selectors to style links with particular classes, and you don’t have to roll back your basic link styles because those classed links aren’t matched by the first selector.

slide: .trigger + * matches the sibling element that follows the one with the trigger class applied
The adjacent sibling selector is old news…

Here’s our first new selector that isn’t a pseudo-class!

Most of you are probably familiar with +, the adjacent sibling selector. It lets you write a selector to match the element immediately following another element, that shares the same parent element.

slide: .trigger ~ * matches all of the sibling elements following the one with the trigger class applied
…but the general sibling selector is new and shiny!

Well now we have ~, the general sibling selector. It lets you look for matches among all of an element’s following siblings.

This is another selector that has some surprising uses, as we’ll see in a bit.

slide: “that’s old news!”

Now you might be thinking all these new selectors have been around for years, and you’d be right; they have. So let’s look at some really new selectors.

slide: [attribute=value] [attribute~=value] [attribute|=value] [attribute^=value] [attribute$=value] [attribute*=value]
These attribute selectors have been around for awhile…
slide: [attribute=value i] [attribute~=value i] [attribute|=value i] [attribute^=value i] [attribute$=value i] [attribute*=value i]
…but now you can add a letter ‘i’ to the end to make the values case-insensitive

This is boring. I honestly don’t know why you would want this, so let’s move on.

slide: :matches() Example: ol > li :link, ol > li :visited, ul > li :link, ul > li :visited can be rewritten as :matches(ol, ul) > li :matches(:link, :visited)
The :matches pseudo-class lets you reduce duplication in your selectors, by selecting one of two alternatives

The selectors above match visited or unvisited links inside of list items, which are children of an ordered or unordered list. The first version is quite repetitive because of the alternatives. If you’re familiar with Sass, you can eliminate this repetition using nesting. But now, you can do the same thing in pure CSS using :matches.

But this is really just syntactic sugar. It doesn’t let you select any elements that you couldn’t select before. So, boring.

slide: :placeholder-shown
:placeholder-shown lets you style a form field when it’s currently displaying a placeholder

Boring!

Why There’s Nothing New

Permalink to Why There’s Nothing New
slide: why there’s nothing new

So, why are there no new CSS Selectors worth talking about? I think it’s because we’re not asking for them, because we’re all doing things like this:

slide: Bootstrap A search form is styled entirely by adding class attributes that apply ready-made styles to HTML elements
You can create a perfectly serviceable search form using Bootstrap classes

But even if you’re writing your own styles, chances are you’ve recently adopted a methodology like this one:

slide: BEM .everything__is-a--class
Block Element Modifier (BEM) recommends giving every HTML element you might want to style a unique class

Even CSS Modules is just an easy way to generate unique class names:

slide: CSS Modules A set of generic class selectors (.menu, .item, .active) in a stylesheet are compiled to unique class names (.site_nav__menu--8675e09, .site_nav__item--8675e09, .site_nav__active--8675e09
The class names in the CSS module on the left get compiled to unique class names that are then inserted into your views

Let me be clear, we’ve adopted CSS Modules at Culture Amp and they’ve changed our game. We love them. But they really just make it easier to use class selectors.

slide: We are abandoning HTML semantics
When we use selectors like these, we are abandoning HTML semantics when it comes to our styles

In other words, we are no longer styling elements because of what they actually are, but because of what we have demanded they look like, using classes.

slide: Not a button: <span class="button"> Click me! </span>
…and that can lead to disconnects between the appearance and the behaviour of elements like this one

Whether or not you think this is a bad thing probably depends on whether you’re building something with the same semantics as HTML—text documents! If you’re building a GUI application that runs in the browser, it’s harder to justify sacrificing the control of classes in support of HTML semantics. You’re probably more concerned about ARIA attributes than element types.

But for better or worse, we’re turning into these people:

slide: Bryce Shivers and Lisa Eversman stand in an open doorway with their bird-painting kits in hand
We’re becoming the “put a class on it” people

We show up, we want to make our page “all pretty”, so we put classes on things until we’re done. What kind of craftspeople are we if we always use the same tool for every job?

slide: prescriptive vs axiomatic, unintelligent vs intelligent

Heydon Pickering, to whom I’m indebted for several of the techniques in this talk, has proposed that we write selectors that respond to things like the relationships between elements, rather than prescribing the styles of individual elements, one at a time. These ‘axiomatic’ or intelligent styles can give rise to useful, emergent behaviour. I don’t have a lot of time to go into this, but if you’re interested please do check out his talk, Effortless Style. The video is online for free and it’s worth your time.

slide: CSS Selectors Level 4 Working Draft
Our over-use of class selectors is having a profound impact on the future of CSS

Let’s take a look at a couple of features in the current CSS Selectors Level 4 Working Draft:

slide: :local-link
:local-link applies styles only to links that remain within the current site

Now this would be really useful; I would totally use this! But there’s no support for it in browsers and no sign of it coming.

slide: Reference Combinator Example: label:hover /for/ input
When the user hovers over the label, the input field is styled because the label’s for attribute references the input’s ID

If you’ve heard of the CSS checkbox hack, well this creates all sorts possibilities for interactive styles. This would be amazing. But there’s no support for it in browsers and no sign of it coming.

slide: Selectors Level 4 Editor’s Draft, 22 March 2016. “Removed the ‘:local-link’ and reference combinator for lack of interest.”
In fact, both of these features have just been removed from the Selectors Level 4 Editor’s Draft due to “lack of interest”

That’s the ‘lack of interest’ of you, me and everyone in this room reading this.

Here’s a feature that’s still in the Editor’s Draft:

slide: :has() Example 1: a[href]:has(> img), Example 2: section:not(:has(h1, h2, h3))
Otherwise known as the ‘parent selector’, :has() is the holy grail of CSS selectors

I’ve been wanting this for years. In the first example, a[href]:has(> img) matches any link that contains an image as its immediate child. Right now you could match that image, but if you wanted to style the link, you’d need to add a class to it.

The second example, section:not(:has(h1, h2, h3)) matches any section that does not contain any headings. It’d take a gnarly bit of template logic to get your CMS to add a class for this to the right sections automatically.

But as with many things in life, you need to read the fine print:

slide: Dynamic profile: “…appropriate in any context, including dynamic browser CSS selector matching. Includes every selector … except for: the :has() pseudo-class” Static profile: “…appropriate for contexts which aren’t extremely performance sensitive [such as] a static document tree. For example, the querySelector() method.”
The :has() pseudo-class is the only selector relegated to the ‘static profile’

Elsewhere in the CSS Selectors Level 4 draft, the concept of the ‘dynamic’ and ‘static’ profiles is introduced, and unlike every other selector in the spec, the :has() pseudo-class is restricted to the static profile, which means you can only use it in JavaScript, not in your CSS stylesheets.

Browser vendors say it has to be this way, because it’s impossible to apply selectors that contain :has() dynamically, as a page is progressively rendered and then modified.

Because we’re all just settling for class selectors, there’s a voice missing from this debate: the voice of developers like us who have wanted this kind of power and flexibility for as long as we’ve been writing CSS.

In my nearly 20 years as a professional developer, I’ve lost count of the number of times I’ve told my boss something was impossible, but once I was given a push I found a way to make it happen. Browser vendors may well be right; it could be impossible to implement full support for :has() in a performant way, but I think we should give them that push.

slide: a power drill with a gleaming assortment of sharp bits
As developers, we want power tools…
slide: two bulldog clips and a rubber band
…but what we’re getting amounts to little more than paper clips and rubber bands

Incidentally, this is the most depressing piece of stock photography I’ve ever paid money for, and I blame all of you for it.

Thankfully, I know someone who’s pretty good with paper clips and rubber bands.

MacGyver is a TV action star from the 80s, who used science to— Well, it’s probably easier just to show you:

That’s MacGyver rescuing a hostage from a terrorist camp in a bamboo airplane built from parts he found around the tent in which they were being held.

So if all we have are paper clips and rubber bands, let’s see if we can make a couple of bamboo airplanes.

slide: * + *
This is the Lobotomized Owl selector
slide: Lobotomized Owl * + * { margin-top: 1.5em; }
The Lobotomized Owl selector (* + *) makes it really easy to create consistent block spacing

Invented by Heydon Pickering, this selector combines two universal selectors (*) with the adjacent sibling selector (+). It matches the second of any two adjacent elements, but by applying a top margin, which only affects block elements, you’re only styling adjacent blocks.

It works well with nested blocks, too
slide: :not(:first-child)
Isn’t this equivalent?

If you know your selectors, you might be thinking that * + * could be more clearly written as :not(:first-child). Well, I’d tell you that you were almost right. First, it would need to be :not(:first-child):not(:root) so that it didn’t match the body element.

slide: :not(:first-child):not(:root) IE9+, high specificity
This alternative form requires Internet Explorer 9 or later, and has surprisingly high specificity

With two pseudo-class selectors in this alternative form, you’d have a selector with relatively high specificty, that was difficult to override with other selectors. * + *, on the other hand, has a specificity of zero. Any selector will override it!

For example:

slide: p + p { margin-top: 0; text-indent: 2em; }
Instead of a top margin, use a text indent on adjacent paragraphs

Isn’t that cool?

Here’s another bamboo airplane, also thanks to Heydon Pickering.

slide: quantity selectors

Since I’m presenting this at Respond, I don’t need to tell you what media queries are. They let you apply different styles depending on the size of the device’s viewport.

slide: media queries display: table-cell;
Here we have a nav bar where the items are styled as table cells
display: inline-block;
As the browser window gets smaller, we might switch to inline blocks to fit them all in
display: block;
And on phone-sized devices, we could stack them vertically as blocks

But what if we could write styles that responded to the content of the page the same way media queries let us respond to the size of the browser window?

Let’s say we have the same nav bar.

slide: quantity selectors display: table-cell;
Table cells work well when there are just four items
But if one of your content people adds a fifth category to your content management system, things get a bit tight
Add a sixth item, and the layout breaks
display: inline-block;
We’d like to respond to the number of items by switching to inline-block…
…and this layout could also accommodate a greater number of items

So, what do you think? Can we do this?

Let’s start simple:

slide: one element li:only-child
Using :only-child, we can change the styles applied to the items of a list if there is only one of them
slide: li:not(:only-child)
We can also reverse the logic, with :not(:only-child) matching all of the list items, but only if there are more than one of them

Okay, that was too easy. Let’s try a bigger challenge:

slide: n elements
Can we come up with a selector that will apply different styles to the list items if there are exactly six of them?
slide: li:nth-last-child(6):first-child
This selector targets the first child, when there are exactly 6 children

:nth-last-child(6) matches the sixth-to-last child, and :first-child matches the first child. For one element to be both the sixth-to-last child and the first child, there must be exactly six children!

slide: li:nth-last-child(6):first-child ~ li
This selector uses the same trick, and targets the rest of the children

Earlier we saw ~, the general sibling selector, which matches all of the following siblings of a given element. Since we’ve already got a selector to match the first child, we can use this to write this second selector that matches the remaining five children, but again, only if there are exactly six children.

slide: li:nth-last-child(6):first-child, li:nth-last-child(6):first-child ~ li
We can now combine these two selectors to achieve the desired effect

Not bad! But the challenge before us is to write a selector that matches n or more children.

slide: n or more elements
We’d like our alternative styles to apply if there are six or more children
slide: li:nth-last-child(n+6)
This selector matches the 6th-to-last child, the 7th-to-last child, and so on

If there are fewer than six children, this selector won’t match anything. So now that we’ve matched the first several, all we need is a selector to match the remaining five children. And I just showed you how to do that!

slide: li:nth-last-child(n+6) ~ li
Adding the general sibling selector, we can match the remaining children

And there you have it:

slide: li:nth-last-child(n+6), li:nth-last-child(n+6) ~ li
Combine the two selectors, and we’ve done it

These two selectors combined will match all of the children, but only if there are six or more of them!

We can also invert the logic, and write a selector that matches all of the children, but only if there are n or fewer of them:

slide: n or fewer elements li:nth-last-child(-n+5):first-child, li:nth-last-child(-n+5):first-child ~ li
See if you can explain to yourself how this works

There we go: the Lobotomized Owl and Quantity Selectors. Two bamboo airplanes assembled from parts we found lying around.

I hope I’ve rekindled your interest in CSS Selectors, and encouraged you to get out there and demand the powerful selectors we need.

slide: CSS Selectors Redux, Kevin Yank, @sentience, kevin@cultureamp.com, Culture Amp is hiring in Melbourne!