Today’s CSS makes styling your websites so much easier than before, and thanks to modern selectors we can use a lot less code to accomplish a lot more functionality! While new features like scroll driven animations and grid tend to get more attention online, these selectors are my most-used bits of modern CSS. They’ve even replaced a lot of functionality that used to require Javascript.
Here are some of my favorite selectors that I use on pretty much every project. All of the following examples come from actual live websites I’ve coded in the past, some are used on this very site if you do a bit of inspecting!
:has()
My most used (and most favorite) modern selector is :has() – if you follow me on Mastodon you’ll know about my “odes” to :has() and it’s incredible flexibility.
:has() allows you to check a parent element to see if it contains a specific class, id, state, or other element. For example, if I want to style a hero that has a heading and featured image element differently than when it’s just the heading alone, I could use :has() to check for this and let the heading span the entire width if it’s alone:
.parent-name h1 {
grid-column: 1 / -1;
}
.parent-name:has(img) h1 {
grid-column: span 6;
}
You can also combine and chain other selectors with it. This selector checks to see if there are any siblings previous to the one with a .slick-active class (great for animated growing lines on a carousel)
.parent-name li button {
background: transparent;
}
.parent-name:has(~ li.slick-active) button {
background: black;
}
:not()
My second most used and favorite modern selector is :not() – :not() is basically the opposite of :has() in that it checks to make sure there is not any element, state, etc inside of that parent.
filters:not(:has(:checked)) a {
opacity: .5;
}
You can chain :not() as much as you want to make some crazy combos! I used this one to check for modules with any background colors ([class*=”bkg-“]) that didn’t have the rounded class (:not(.bkg-rounded)) or an extra margin spacing class (:not([class*=”margin-spacing-“]) that came after another module with any background color but no rounded class (phew, that was a mouthful!). This is great for modular layouts because we can really make sure all of them place nice together.
.module[class*="bkg-"]:not(.bkg-rounded) + .module[class*="bkg-"]:not(.bkg-rounded):not([class*="margin-spacing-"]) {
padding-top: 0;
}
:nth-child(x of .class)
The one issue I’ve always have with :nth-child and related selectors is that there was no way to choose a numbered child by class. If we have a area with 6 different sections with mixed classes:
<section class="hero"></section>
<section class="testimonial"></section>
<section class="text"></section>
<section class="image"></section>
<section class="text"></section>
<section class="value-props"></section>
But we only wanted to choose the 2nd .text section – well that was impossible. Asking CSS to do something like this would not work here unless .text was actually the second element in the parent:
.text:nth-of-type(2) {
/*Nothing happens because the second section element has a class of .testimonial, not .text */
}
Thanks to (x of .class) though, we can now specifically choose the second element in a parent that has that class name regardless of what number in the list of elements it actually is! So if we wanted to target the second instance of .text in the example above:
:nth-child(2 of .text) {
background: pink;
}
I use this selector a lot less than the previous two, but it’s still in my tool belt and very useful.
:empty
:empty can be a bit tricky but it’s very useful when it works. All it does is target an element that’s….empty! No children, text, or elements inside whatsoever. I mention it’s tricky because if there’s even a blank space, this selector will not work.
I like using :empty to hide parents before some kind of ajax interaction. For example, here’s an element that will hold active filter pills when a user chooses a specific category on a blog archive:
<div class="active-pills"></div>
We’ll hide it when :empty so it’s margins and paddings don’t make extra space when there’s nothing inside:
.active-pills {
margin: 20px 0;
padding: 20px;
}
.active-pills:empty {
display: none;
}
Using :empty here allows you to rely a lot less on Javascript and adding active states or extra classes.
:only-child
:only-child is another selector that I use rarely, but is always on hand for special styling needs. It only applies the styles if the element is the only child of the parent. Here we’ll apply different grid columns depending on if the article is an only child or not of a parent.
article {
grid-template-columns: repeat(auto-fit, minmax(100px, .33fr));
}
article:only-child {
grid-template-columns: .5fr;
}
Mixed Selector Examples
Now for the really fun part! Yes you can mix and match all of these together! Here are some more complicated real examples I took from previous projects.
Want to target an element that does not have a 4th element with a class of .text? Easy!
.parent-class:not(:has(:nth-child(4 of .text))) {
}
Want to target a button only if there’s not a checked/selected input? It used to require JS, but no more!
.filters:not(:has(input:checked)) button {
}
How about targeting the parent when it only has one child that happens to be an article? Yessiree!
.section:has(article:only-child) {
}
Wanna choose the last child in an element, but only if it’s not the only child?
span:last-child:not(:only-child) {
}
I always enjoy seeing how much I can chain together while still making it readable to humans (I am a nerd, yes).
Go Forth and Select
Using these new selectors allows you to reduce your dependency on JS, classes, and IDs – which make for a more performant website with reliable styling for modular layouts. All of these are fully browser supported (and have been for some time now), so you can start using them in your own projects.
Got some crazy selector recipes of your own? Please send them to me on Mastodon, I love trying to parse them all out!