Tutorial: CSS Selectors and Their Specificity

Lara Aigmüller

Last time we added some basic CSS to our website—this time we won’t continue working on our website’s stylesheet, but we will (in contrast to the somewhat boring title of this post) still cover very important basics on how CSS works.

CSS selectors

A CSS selector is the first part of a CSS rule. It's a description of a component in our HTML code that the respective style declarations should be applied to. There are many possibilities how to select HTML elements; here are the three most important ones and some information about pseudo-classes:

Type selectors

Type selectors are the ones we learned about in the last session about CSS basics. A type selector is the name of an HTML tag, for example h1 to select a level one headline, or footer to select a footer element.

CSS rules using type selectors look as follows:

h1 {
  font-size: 2.5rem;
  color: tomato;
}

p {
  font-size: 16px;
  color: #424242;
}

Type selectors are useful to set basic global styles for your website or application, for example the main text color, spacing between headlines and text, the site’s basic background color and so on.

Class selectors

To learn about class selectors we first have to talk about what a class is. Every HTML element can get a class by adding the equally named attribute to it. Easy as that:

<header class="article-header">
  <!-- some content -->
</header>

Why can't we just use the header type selector to apply styles to this element? Think about what happens when there are more headers on a page, e.g. the main page header, several article headers on a blog post overview page, and additionally a header for the sidebar. Usually, we want the article headers to look different than the page or aside headers. This is where our class selector comes in. A class selector always starts with a period . followed by the class name used in the HTML code.

.article-header {
  padding: 1rem;
  text-transform: uppercase;
  background-color: darkslategray;
  color: #ffffff;
}

We can make use of class selectors to add more specific styles for our .page-header, .article-header or .aside-header instead of using one global style for all header elements.

It could be that there are styles all headers have in common. These style declarations can be put in a rule using the header type selector. More specific styles are defined by using class selectors.

header {
  text-transform: uppercase;
  color: #17A964;
}

.page-header {
  font-size: 24px;
}

.article-header {
  font-size: 18px;
}

.aside-header {
  font-size: 16px;
  font-weight: 700;
}

The <header class="aside-header">…</header> element would get the color and text-transformation styles from the CSS rule using a type selector and additionally the defined font-size and font-weight from the CSS rule starting with the .aside-header class selector.

Pseudo-class selectors

You probably know from your own experiences on the web that elements on a website can have different states. When you hover your mouse over an interactive element (a link or a button for example), its appearance usually changes.

You should always provide different styles for different states of elements to highlight possible interactivity and draw the user’s attention to what’s happening on the page.

To do so, we can make use of so-called pseudo-class selectors. A very common one is the :hover pseudo-class.

a {
  color: #17A964;
  text-decoration: none;
}

a:hover {
  color: #12864F;
  text-decoration: underline;
}

For all links on my website, I reset the default behavior of having underlines by adding the text-decoration: none; declaration. Additionally, I give them a color different from the body text color. On hover, the color changes and an underline is shown.

Pseudo-classes are especially useful in forms where inputs can have different states, e.g. :checked checkboxes, or :disabled input elements, or :focus’ed ones. A comprehensive list of available pseudo-classes can be found in the MDN web documentation. When using HTML elements as intended, all browsers come up with default styles for these important states. Once you start adding your own styles, make sure to take care of styling interactive elements in all their possible states.

A small HTML form with a normal input, a focused input, a checked checkbox, a normal, and a disabled button displayed in a web browser.

Focus states and accessibility

One important side note on focus states: Very often, I find websites and applications resetting the browser’s default focus states by adding :focus { outline: none; } to their CSS code, because “it doesn’t look good” or “it doesn’t fit the website’s design” (the outline’s color depends on the operating system and browser used).

You can do that, but never ever(!) remove all default focus styles on your website without adding well-designed, hand-crafted focus states again for all interactive elements that can have a focus state! Users navigating your website or application with their keyboard instead of the mouse will be completely lost because there won’t be any feedback about where they are on a page (i.e. which element currently has focus) and this is very bad when it comes to accessibility.

When you don’t want to invest time providing your own focus styles, leverage what you get for free from browsers by writing semantically correct HTML and use attributes as intended.

ID selectors

When building an accessible HTML form we already learned about the id attribute an HTML element can have. This can be used within CSS to select an element. ID selectors start with a hash #.

<nav id="main-navigation"></nav>
#main-navigation {
  width: 100%;
  padding: 2rem;
}

Class selectors vs. ID selectors

The usage of class- and ID-selectors is similar. You use them to provide more specific styling for selected elements. There’s one main difference, though: IDs have to be unique and can be used only once on a page. The same class name can be used several times.

Take a news page for example: you only have one main page header, but depending on the number of articles you display on this page, you can have multiple article headers. For the main page header you could use an ID to identify the element, for the article headers you use classes.

<header id="main-header">
  <h1>Latest news</h1>
</header>
<!-- more content -->
<article>
  <header class="article-header"></header>
</article>
<article>
  <header class="article-header"></header>
</article>

I rarely use ID selectors in my CSS code for another reason called specificity: an ID selector is more specific than a class selector. Look at the following (admittedly rather useless) CSS and HTML code:

header {
  background-color: darkslateblue;
}

.my-header {
  background-color: salmon;
}

#my-specific-header {
  background-color: darkseagreen;
}
<header class="my-header" id="my-specific-header"></header>

All three CSS rules from above would apply to the header element because all three—the type selector, the class selector, and the ID selector—match. Obviously, an element can only have one background color. The winning color in this example is… drum roll 🥁 … darkseagreen, because the ID selector is the most specific of all three.

This was a rather easy example. Specificity (and not understanding it) is one major topic why people struggle with CSS. That’s why there will be more about it later in this post. Stay tuned!

Grouping and combining selectors

Code examples from the CSS basics post already cover grouping selectors using a comma , between selectors. The following rule applies to all headline levels.

h1, h2, h3, h4, h5, h6 {
  color: indigo;
  font-weight: 700;
}

Instead of creating a CSS rule for each headline we avoid code duplication by grouping selectors.

Additionally, we can combine selectors in different ways. This is useful and required if we want to get more specific with our styles. (Here’s the “specific” word again. Yes, it’s a thing…)

Descendant combinator

Imagine we have styled all links on our website so they look good on white background. The background color of our page footer is dark gray and our defined link color doesn’t work here (because the contrast would be too bad). This is how we could solve it in CSS:

a {
  color: steelblue;
}

footer {
  background-color: darkslategray;
  color: white;
}

footer a {
  color: white;
}

Separating the footer and a type selectors by a single space means: apply the styling to all a elements which are descendants of a footer element.

Child combinator

A similar way of combining selectors is the child combinator. The difference to the descendant combinator is that the style is only applied to direct children and not any descendant.

<nav>
  <a href="/">Home</a>
  <a href="/about">About</a>
  <a href="/contact">Contact</a>
</nav>

In our navigation we have three links which are direct child elements of the nav element. The following CSS code would change the links’ appearance:

nav > a {
  color: tomato;
  text-transform: uppercase;
  font-weight: bold;
}

As soon as you change the HTML structure to something like this (because you need an extra div element for styling)…

<nav>
  <div>
    <a href="/">Home</a>
    <a href="/about">About</a>
    <a href="/contact">Contact</a>
  </div>
  <div>
    <a href="/profile">User profile</a>
  </div>
</nav>

… the CSS would have no effect anymore, because the links are now direct children of the div element and not the nav element.

It may seem that the child combinator is not useful at all compared to the descendant combinator, but there are some interesting use cases once we are going to wrap our heads around CSS layouts where code structure matters.

Adjacent sibling combinator

Instead of applying styles to children they can also be applied to siblings like this:

h2 + p {
  margin-top: 2rem;
}

The rule above means: every paragraph p immediately following a level two headline h2 should have a top margin of 2rem. The top margin is the distance between an element and the element above.

In the following HTML example the rule will be applied to the first paragraph, but not the second one as it’s no direct sibling of the headline.

<h2>About me</h2>
<p>Here's some text about me. The distance between the headline above and this paragraph is 2rem as described in our CSS.</p>
<p>Here is another paragraph with no explicit top margin set.</p>

General sibling combinator

When we want to apply the style to all siblings of our level two headline we can use the following piece of code:

h2 ~ p {
  margin-top: 2rem;
}

The syntactical difference is the usage of a tilde ~ instead of a plus +. The practical relevance of the above example is open for discussion, but once we’re talking about CSS layouts there will be examples where this type of combining CSS selectors is one way to go.

Specificity

I mentioned words like “specific” or “specificity” a few times in this post already, but what does this mean and why do we need to know? 🤔

We saw an example above where more than one rule matched a certain HTML element on our page. This usually happens a lot when CSS files grow bigger. In such a case we (or, to be precise, the browser) need(s) to know which rule to apply. This is where specificity comes into play.

CSS selectors have a certain specificity. Type selectors are less specific than class selectors; class selectors are less specific than ID selectors. When selectors are combined specificity is increased. I like to use the following method (I first saw this in a workshop by Miriam Suzanne) to calculate a selector’s specificity:

Let’s say we have this selector in our CSS: #main-navigation .list-item a; its specificity can be written as 1.1.1 (1 ID selector, 1 class selector, 1 type selector).

Another selector #main-navigation .nav-list .list-item a has the specificity 1.2.1 (1 ID selector, 2 class selectors, 1 type selector). When comparing the specificity score we first look at the first number. When it is the same we go on to the next number. Here 2 beats 1, therefore, the selector in the second example is more specific.

Another example that you might find in other code bases (but should try to avoid when writing CSS on your own, because it already gets too specific): .drop-down-text .drop-down-icon .title span which results in a specificity score of 0.3.1. You can override CSS declarations following this rule by using an ID selector, e.g. #drop-down-icon with a score of 1.0.0. Comparing the first numbers shows that the second selector is more specific, just because it uses an ID selector.

We could go on with examples here, but for now, I guess, you got a basic understanding of how specificity works.

The less, the better

When people don’t know the full set of CSS tools, they tend to overuse style declarations in order to reach their goals. Consequently, they create more and more CSS rules and start overriding things they’ve written before. This leads to bloated CSS code and overly specific selectors.

We should always try to write as little CSS code as possible and avoid too specific selectors for some reasons:

  • Maintenance gets more difficult (things tend to break when you restructure your HTML code and rename or remove elements)
  • Overriding styles results in even more specific selectors (to override a certain CSS rule we need a selector even more specific)
  • Other developers need more time to understand the code (and maybe the author themselves after some time of not working on it)
  • Code bases can get bloated because nobody dares to remove convoluted styles
  • Bug hunting takes longer

When working on your CSS and you just can’t get the desired result, take a step back, remove parts of the code, and add it back line by line to find out where you’re going wrong. This might not make sense to you now, but probably somewhere on the way. 😉

Key takeaways

This part of my tutorial series was a bit different, as we didn’t work on our web page, but was important nonetheless. Here’s what you should remember:

  • There are different kinds of CSS selectors: type selectors, class selectors, pseudo-class selectors, ID selectors
  • Selectors can be grouped and combined. Available combinations are: descendant combinator, child combinator, adjacent sibling combinator, general sibling combinator
  • Selectors have a certain specificity based on which a specificity score can be calculated
  • Specificity increases based on the combination of selectors used
  • More specific selectors override less specific ones
  • Less is more, no matter if we’re talking about CSS code size in general or selector specificity

We did not cover all selector types and there is even more to discover about specificity. That’s why this series will continue soon! 😊 Looking forward to next time when we will learn about the CSS box model and get started with CSS layouts.

Any thoughts or comments?

Say hello on Mastodon or contact us via email or contact form!