DEV Community

Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

Level up your CSS selector skills

I’ve been using CSS for many years now but one thing I’ve not revisited in depth until recently is the topic of CSS selectors.

Why would I need to do this? We all know selectors inside out by now, right?

The trouble is that (at least in my case) over time it’s easy to get used to using the same trusted set of selectors on every project to achieve what you need to do.

So I took it upon myself to do an in-depth review of CSS selectors and came across some interesting ones that were either new to me or were used in a way that hadn’t occurred to me before.

I also discovered some cool new selectors that will be available in the future but aren’t yet widely available.

I invite you to join me as I take a look at various types of CSS selectors. How many of these are you already using in your day-to-day work? I’d be interested to know.

Ready to level up your CSS selector skills? OK then, let’s go.

Combinator selectors

Let’s start in familiar territory. Combinator selectors are used to select child elements as well as siblings and have been around for a quite a while now.

  • General child selector (space). e.g. A B
  • Direct child selector. e.g. A > B
  • Adjacent sibling selector. e.g. A + B
  • General sibling selector. A ~ B

The adjacent selector A + B should be familiar to you and selects the element B which immediately follows A. But what about the general sibling selector A ~ B? This selects all sibling elements B that follow A.

Here’s an example of them both in action:

https://medium.com/media/62c0884db0465a2fafb41d8f3a16a665/href

The New York row is selected because it immediately follows the first row, and the last two cities are highlighted as the general sibling selector matches all cities after the 4th one.

Attribute selectors

I really like attribute selectors. They’re just so flexible when you need to match elements containing attributes with specific values.

https://medium.com/media/3df63743e5520581894d5df62e87d715/href

This example demonstrates selecting all checkbox input elements and applying styles to their associated labels to make them bold and colored blue.

We then override the style for a checkbox with the specific name chk2 and color its associated label red. Notice how the other form element labels are unaffected and don’t have label styles applied.

Attribute selectors aren’t just for form elements though, they can target attributes on any element. And you can match any attribute not just officially supported ones. Furthermore, you can just check for the existence of an attribute as follows:

button[icon]

This matches

Some more examples:

https://medium.com/media/c15a68ed07c93e48ceca45d618772c10/href

The first link doesn’t have a target attribute so isn’t matched. The next two links are matched because they either have a blank target attribute or one with a specific value. Finally, the last link is set to pink as it matches the fluffy attribute. Its value is irrelevant and just has to exist to match the a[fluffy] selector.

A practical example of this could be to highlight images that don’t have an alt attribute. This attribute is required for accessibility so its important for SEO purposes that you make sure all image elements contain this attribute.

We can use the following rule to achieve this:

img:not([alt]) {
 border: 2px red dashed;
}

https://medium.com/media/87765a30b26a1316cb829e604af99432/href

If you want to match a specific part of an attribute value then there are some very useful selectors available.

A[attr=val] — Attribute begins with value.

A[attr|=val] — Attribute begins with value OR is first in a dash separated list.

A[attr$=val] — Attribute ends with value.

A[attr*=val] — Value occurs anywhere in attribute.

A[attr~=val] — Value matches attribute in space separated list.

Here’s an example of each one:

https://medium.com/media/b38466141b43ade5415217ee13f309cb/href

The first two examples are very similar except A[attr|=val] also matches the value followed by a dash-separated string. This can be useful for matching language attributes. e.g.

.

Matching file extensions is made easy with A[attr$="val"] and coupled with ::after you can easily display the matched file too. Note the use of attr() and concatenation to join it with a static string.

The A[attr*=val] shows how you can match a specific domain no matter what protocol or subdomain is used.

Finally, we have A[attr~=val] which is great for matching a value in an attribute made up of a list of values separated by spaces. This only matches the whole word not word fragments as the *= operator does so word plurals won’t match.

All of the above examples of attribute selectors are case sensitive. But we have a trick up our sleeve. If we insert an i before the closing square brackets we can switch on case-sensitive matching.

https://medium.com/media/f29fd8e85539763d32c07f0aa3c3a71f/href

Most of the major browsers support case insensitive matching except Internet Explorer and Microsoft Edge.

User interface selectors

If you’ve worked on styling forms then you’ve undoubtedly encountered these type of pseudo-classes before:

  • :enabled
  • :disabled
  • :checked

For example we can use :checked to style a simple to-do list.

https://medium.com/media/f99000113072f13e53f88b347886757e/href

This is pretty standard but there are some other interesting pseudo-classes that we have at our disposal.

:default matches one or more elements that is the default in a group of related elements. This can be combined with the reset button type too.

https://medium.com/media/bae750c17c2114ff9af8a408c896e66c/href

We can use pseudo classes to match whether input values are valid or not directly with CSS, as well as checking if any elements are required before the form can be submitted.

If you start typing into the personal email input field then it has to be valid. However, the work email address is always required and needs to be valid so can’t be left empty. Notice too how we can chain pseudo classes (e.g. :required:invalid) to achieve what we need.

Next we have two pseudo-classes that can match if a form element (that supports min and max attributes) is in range or not.

Again, we can use the reset button type to reset the default value of the number input element.

To round off this section let’s take a look at the :read-only, :read-write, and :placeholder-shown pseudo classes.

https://medium.com/media/3f99dcfdf3cf122ee23a744ab3bf80b8/href

Using these allows you to easily match elements that are read-only, or writable (editable). Matched elements don’t have to be form input fields either as demonstrated in the example Pen.

Finally, :placeholder-shown will match elements that haven’t been interacted with yet and still display the default placeholder text. This particular selector should be used with caution as it’s not widely supported yet.

Structural selectors

Structural selectors are very powerful and match elements based on their position in the DOM. They give you the flexibility to match elements purely with CSS that would otherwise require JavaScript to do the same thing.

This type of selector is different from the ones show so far as some of them allow you to pass in a parameter to modify how the selector works.

For example :nth-child() receives a value that will match a specific child element relative to its parent container.

So, if we had a list of items the following selector would match the third item:

ul:nth-child(3)

However, the parameter doesn’t have to be simple number it can be a simple expression instead that makes the pseudo class even more powerful.

Valid expressions are:

  • ul:nth-child(2)— Matches the second child element
  • ul:nth-child(4n) — Matches every 2nd child element (4, 8, 12, …)
  • ul:nth-child(2n + 1) — Matches every 2nd child element offset by one (1, 3, 5, …)
  • ul:nth-child(3n — 1) — Matches every 2nd child element offset by negative one (2, 5, 8, …)
  • ul:nth-child(odd) — Matches odd numbered elements (1, 3, 5, …)
  • ul:nth-child(even) — Matches even numbered elements (2, 4, 6, …)

The expression variable n always starts a zero so to work out exactly what elements will match start with n as zero, then n as 1, and so on to compile a list of elements.

You can use simple expressions with the following structural selectors:

:nth-last-child() and :nth-last-of-type() are very similar to :nth-child() and :nth-of-type() except that they match from the last element rather than the first.

You can get quite creative with selectors by playing around with various combinations. For example, the previous Pen example contained the selector:

ul:last-of-type li:nth-last-of-type(2)::after {
  content: “ (2nd from end)”;
  /\* Other styles… \*/
}

This matches the pseudo-element that comes after the second from last list item inside the second unordered list. If you’re ever struggling to decode a complicated selector then it’s best to read it from right to left so it can be deconstructed logically.

The next set of selectors are specialized structural selectors as they match specific child elements only. You can’t pass expressions to them to modify their behavior.

At first glance, there’s a lot going on here and you need to be careful when using these type of selectors as you might get results you weren’t expecting.

For example you might be wondering why the And so on… text is blue inside the tag. Actually all the section content is blue as it’s the last child of the main div container. Other section elements have their own colors overridden via other selectors leaving the single paragraph colored blue.

Content selectors

These belong to a specialized set of selectors for matching content. The ones available for us to use right away are:

::first-line and ::first-letter only work if applied to block level elements. Also be careful to use ::first-letter only on specific elements otherwise every single paragraph would have a drop cap which is probably not what you want!

There some exciting content selectors in the works which aren’t available right now, but when they are supported they’ll open up all sorts of possibilities.

Here’s a list of content selectors to watch out for:

  • ::inactive-selection — Selected content inside an inactive window
  • ::spelling-error — Check spelling and grammar for editable elements
  • ::grammar-error — Matches grammatical errors
  • ::marker — Matches list item markers
  • ::placeholder — Matches placeholder text of form elements

Miscellaneous selectors

We’ve just got time to mention a couple of other selectors that didn’t fit into any of the previous categories. Don’t worry we’re almost done! Unfortunately, most of these are experimental so you’ll have to wait a while to use them in production.

The :target selector targets an element with an id that matches part of the current URL. So if we had an element with an id of part1 and the URL:

[https://mysite.com#part1](https://mysite.com#part1)

We could match that element with:

:target { border: 1px red solid; }

If you have a large selector then :matches() can help simplify it. For example if you had the following selector:

nav p.content,
header p.content,
main p.content,
sidebar p.content,
footer p.content {
  margin: 15px;
  padding: 10px;
}

Then can be simplified with :matches() and is equivalent to:

:matches(nav, header, main, sidebar, footer) p:content {
margin: 15px;
padding: 10px;
}

Nice! This will help to make style sheets much more readable.

Next we have :any-link which is a convenience selector and does the same as :link and :visited combined.

So these two selectors would effectively be the same:

:any-link {
  color: red;
}
:link, :visited {
  color: red;
}

And that brings us to the last selectors we’ll be looking at in this article:

  • :dir()
  • :lang()

Both these relate to the language of your site.

:dir() takes in a parameter ltr or rtl depending on the direction of text you want to match and is only currently supported Firefox currently.

So :dir(rtl) would match all elements with content with RTL direction.

Every element in a HTML document can set it’s own individual language by using the lang attribute.

<div lang=”en”>The language of this element is set to English.</div>
<div lang=”el”>Η γλώσσα αυτού του στοιχείου έχει οριστεί στα ελληνικά.</div>
<div lang=”is”>Tungumál þessa þáttar er sett á íslensku.</div>

The same basic text is entered into three

tags but with the specific country added to the end of the content. Also, the country codes used in the lang attribute represent the corresponding country.
  • en — English language
  • el — Greek language
  • is — Icelandic language

The

elements can be matched using the :lang() selector:
:lang(en) { colore: red; }
:lang(el) { colore: green; }
:lang(is) { colore: blue; }

Here’s a Pen to demonstrate:

https://medium.com/media/370d54c1a8c0aa521aa159dbf136c375/href

And the good news is that the :lang() selector is already supported by all major browsers.

Resources

If you get stuck trying to figure out a selector, or need to dig deeper into the CSS specifications here’s some useful resources that you might want to check out:

And finally…

I hope you’ve found this article useful. I certainly had a lot of fun brushing up on my CSS selector skills and playing around with the various possibilities.

There’s a lot of cool things you can do now with pure CSS that just wasn’t possible a few years ago.

This is exciting for designers who can do some pretty advanced styling and animation in pure CSS, and all without a single line of JavaScript in sight.

Happy styling!

Plug: LogRocket, a DVR for web apps

LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single page apps.

Try it for free.


Top comments (0)