Skip to the content

CSS selectors, from type and class to child, attribute and specificity

Last updated: By Lukas BrandtLesson 14 of 215 min read

A selector is the part of a CSS rule in front of the curly braces. It decides which elements the declarations land on. You have used three kinds already: type selectors like p, class selectors like .btn, and the :hover pseudo-class from the hover lesson. Project 2 needs a few more, and it also needs you to know what happens when two rules disagree.

The examples below all use the header markup from the div and span lesson, so you can test every selector on the same page:

<header class="site-header">
  <a class="logo" href="index.html">Bench Notes</a>
  <nav class="main-nav" aria-label="Main">
    <ul class="menu">
      <li><a href="index.html" aria-current="page">Home</a></li>
      <li><a href="guides.html">Guides</a></li>
      <li><a href="tools.html">Tools</a></li>
      <li><a href="contact.html">Contact</a></li>
    </ul>
  </nav>
</header>

The selectors you will use most

Selector Example What it selects Weight (id, class, type)
Type nav every <nav> element 0, 0, 1
Class .menu every element with menu in its class attribute 0, 1, 0
ID #top the single element with id="top" 1, 0, 0
Descendant .menu a every <a> anywhere inside .menu 0, 1, 1
Child .menu > li each <li> that is a direct child of .menu 0, 1, 1
Grouping .logo, .menu a everything matched by either part each part on its own
Attribute a[aria-current="page"] links whose aria-current value is page 0, 1, 1
Pseudo-class .menu a:hover menu links under the mouse pointer 0, 2, 1
Universal * every element 0, 0, 0
Parts links can earn us a commission. As an Amazon Associate I earn from qualifying purchases.

An element can carry several classes, separated by spaces (class="logo small"), and .logo still matches it. An id must be unique on the page, which is one reason it makes a poor styling hook: you cannot reuse it.

Descendant or child

The space in .site-header a is a combinator. It means "an a somewhere inside .site-header, at any depth". In the markup above, that is the logo plus all four menu links. The > combinator is stricter and only reaches one level down:

/* the logo and all four menu links */
.site-header a {
  color: white;
}

/* only the logo: the menu links sit inside nav, ul and li */
.site-header > a {
  font-weight: 700;
}

Most of the time the descendant selector is what you want, since it keeps working when you wrap something in an extra div later. Use the child selector when nested lists or nested boxes would otherwise pick up a style meant for the outer level.

Grouping with commas

A comma lets several selectors share one block of declarations. h1, h2, h3 { line-height: 1.2; } is three rules written once. Each part keeps its own weight, as if you had written the rules separately. One catch: if any selector in the list is invalid, for instance a typo like .menu a:hovr, the browser throws away the whole rule, not only the broken part.

Attribute selectors

Square brackets select elements by their attributes. You need only a handful of forms:

  • [title] matches any element that has a title attribute at all.
  • [type="email"] matches when the value is exactly email.
  • [href^="https://"] matches when the value starts with https://.
  • [href$=".pdf"] matches when the value ends with .pdf.
  • [href*="guides"] matches when the value contains guides anywhere.

Lesson 19 uses .menu a[aria-current="page"] to mark the link for the page you are on. The attribute is already in the HTML for screen readers, so the CSS hangs off something that has to be there anyway, with no extra class to keep in sync.

Specificity in plain terms

When two rules set the same property on the same element, the browser has to pick one. It decides like this:

  1. Count each selector's weight in three columns: ids, then classes (attribute selectors and pseudo-classes count here too), then element types. The universal selector and combinators add nothing.
  2. Compare the columns from left to right. The first column that differs decides. One id beats any number of classes; one class beats any number of types.
  3. If the weights are equal, the rule that comes later in the CSS wins.

A style="..." attribute on the element beats any selector, and a declaration marked !important beats normal declarations. Both are hard to override later, so keep them for emergencies.

Here is a worked example. All three rules below set color:

a { color: blue; }                 /* 0, 0, 1 */
.site-header a { color: white; }   /* 0, 1, 1 */
.menu a { color: gold; }           /* 0, 1, 1 */

For the Home link, all three match. The a rule loses at once (0, 0, 1 against 0, 1, 1). The other two tie, so the later one wins and the menu links turn gold. For the logo, only the first two match, so it turns white. Swap the order of the last two rules and the menu links turn white as well, even though nothing about the selectors changed.

That example is the whole case for styling with classes. When most rules weigh about the same, you can read the stylesheet from top to bottom and predict the result. A single id selector in the mix weighs more than any number of classes, and the usual fix people reach for, another id or !important, makes the next change harder still.

Try it

Open the online HTML editor, paste the header markup, and put the three color rules in a <style> element above it. Check which color each link gets, then swap the last two rules and check again. For one more test, change .menu a to #top a and add id="top" to the <ul>: the gold comes back no matter where the rule sits.