AniUI Academy

Selectors and Specificity

Type, class, ID, attribute and combinator selectors, plus pseudo-classes and pseudo-elements, and the real rule for which of two conflicting rules wins.

9 min read

Every CSS rule starts with a selector, and so far you've only seen the simplest one: a bare type selector like p. Real stylesheets need to be far more precise than "every paragraph" — sometimes you want one specific element, sometimes every element with a certain class, sometimes only an element in a certain position. This lesson covers the full selector toolkit, plus what happens when two selectors disagree about the same element.

Type, class, ID, and universal selectors

p { color: navy; }          /* every <p> element — a type selector */
.card { padding: 1rem; }     /* every element with class="card" */
#main { max-width: 960px; }  /* the one element with id="main" */
* { box-sizing: border-box; } /* every single element on the page */

A few things worth being precise about:

  • Classes are reusable. The same class="card" can appear on any number of elements, and .card will style all of them.
  • IDs should be unique. An id is meant to identify exactly one element on the page. Browsers won't stop you from reusing one, but #main styling is written as if only one element has that id, and other tooling (like <label for="..."> or JavaScript's getElementById) genuinely expects it to be unique.
  • The universal selector * matches everything. You'll see it used almost exclusively for resets, like the box-sizing reset you'll meet in the box model lesson.

Attribute selectors

Target elements by an attribute they carry, not just their tag or class:

[type="text"] { border: 1px solid #ccc; }   /* exact match */
[href]         { text-decoration: underline; } /* attribute present, any value */
[class^="btn-"] { padding: 0.5rem 1rem; }    /* value starts with "btn-" */

These are especially useful on form inputs, where the meaningful difference between two elements is often an attribute (type="checkbox" vs type="text") rather than a class you'd have to add yourself.

Combinators: relating one selector to another

Combinators describe a relationship between two selectors, rather than matching one selector in isolation.

nav a        { color: white; }   /* descendant: any <a> anywhere inside <nav> */
nav > a      { color: white; }   /* child: only an <a> that is a direct child of <nav> */
h2 + p       { margin-top: 0; }  /* adjacent sibling: a <p> immediately after an <h2> */
h2 ~ p       { color: #555; }    /* general sibling: any <p> after an <h2>, same parent */

The difference between the descendant combinator (a space) and the child combinator (>) is the one people mix up most:

  • nav a matches an <a> at any depth inside <nav> — directly inside it, or inside a <ul> inside a <div> inside it.
  • nav > a only matches an <a> that is a direct child of <nav> — one level down, no more.

h2 + p and h2 ~ p are both about siblings that come after an <h2>, sharing the same parent. + only matches the very next sibling; ~ matches every later sibling, however many there are.

Pseudo-classes: styling a state or position

A pseudo-class selects an element based on a state or a position among its siblings, not something written in the HTML itself.

a:hover        { text-decoration: underline; } /* while the pointer is over it */
li:first-child { font-weight: bold; }          /* the first child of its parent */
li:nth-child(2n) { background: #f5f5f5; }      /* every second child — zebra striping */
input:focus    { outline: 2px solid blue; }    /* while it has keyboard focus */

:nth-child() takes a formula — 2n for even positions, 2n+1 for odd ones, or a plain number like 3 for exactly the third child. It's one of the few places in CSS where you're writing something that looks like simple maths.

Pseudo-elements: styling a part of an element

A pseudo-element targets a sub-part of an element that doesn't exist as its own tag in the HTML — written with a double colon to visually distinguish it from a pseudo-class.

p::first-line { font-weight: bold; }   /* just the first rendered line of text */
.quote::before { content: "\201C"; }    /* insert content before the element's own content */
.quote::after  { content: "\201D"; }    /* insert content after it */

::before and ::after are the two you'll use constantly — they let you insert decorative content (an icon, a quote mark, an arrow) purely from CSS, without adding an extra element to the HTML for something that isn't really part of the page's content.

Specificity: what wins when two rules conflict

When more than one rule targets the same element with the same property, the browser needs a tie-breaker. That tie-breaker is specificity — a weighting based on what kind of selector was used, not how it looks or how long it is.

The real hierarchy, from highest to lowest:

  1. Inline styles (style="..." on the element) — beats everything below.
  2. ID selectors (#main).
  3. Classes, attribute selectors, and pseudo-classes — all equal weight (.card, [type="text"], :hover).
  4. Type selectors and pseudo-elements — the lowest weight (p, ::before).

A selector's total specificity is the count of each category it uses, added up — it is not one category simply beating another by being "more specific-sounding". A worked example:

.card { color: blue; }        /* 1 class  → specificity (0, 1, 0) */
#hero { color: red; }         /* 1 ID     → specificity (1, 0, 0) */
<div id="hero" class="card">What colour is this text?</div>

#hero wins, and the text is red — one ID selector outweighs any number of classes, because ID is a strictly higher category. It doesn't matter which rule appears first or last in the stylesheet; specificity is checked before source order ever comes into it. (The next lesson covers what happens when specificity is genuinely equal — that's where source order finally matters.)

A selector combining categories adds them together:

nav.primary a.active { color: orange; } /* 2 classes + 2 types → (0, 2, 2) */

This beats a lone .card (0, 1, 0) and beats a lone nav a (0, 0, 2), because its combined weight is higher in the class column.

!important: the escape hatch to avoid

You can force a declaration to win regardless of specificity by appending !important:

p { color: blue !important; }

This beats every normal rule, including inline styles, and it's tempting to reach for the moment a rule "isn't working." Resist it. The problem is almost always that some other, more specific rule (or an inline style) is winning for a reason you haven't found yet — and !important doesn't fix that reason, it just papers over it with a bigger hammer. The next person who needs to override your rule now has to reach for !important too, and once two !important declarations on the same property collide, you're back to needing specificity (and then source order) to sort them out, except now in a much more confusing spot. Treat it as a last resort for overriding styles you genuinely can't edit (a third-party widget's CSS, say), not a tool for your own stylesheets.

Try it yourself

Three rules all target the same paragraph below with different selector types. Read the CSS and predict which colour wins before you look at the rendered result — then check yourself against the specificity hierarchy above.

Try it yourself
Loading playground...

What to remember

  • Type (p), class (.card), ID (#main), attribute ([type="text"]), and the universal (*) selector are the basic building blocks.
  • Combinators relate selectors: descendant (space, any depth), child (>, one level), adjacent sibling (+, the very next one), general sibling (~, any later one).
  • Pseudo-classes (:hover, :first-child, :nth-child()) target a state or position; pseudo-elements (::before, ::after, ::first-line) target a sub-part of an element.
  • Specificity order: inline styles beat everything, then IDs, then classes/attributes/pseudo-classes (equal weight), then types/pseudo-elements. Ties are broken by source order — covered next lesson.
  • Avoid !important — it overrides the whole specificity system instead of fixing the rule that's actually winning.

Check yourself

4 questions · pass 3/4 to unlock The Cascade and Inheritance

up to 50
  1. 1.Given .card { color: blue; } and #hero { color: red; } applied to the same element, which colour wins?

  2. 2.Which selector targets only elements that are the first child of their parent?

  3. 3.What does div p (with a space) select, versus div > p?

  4. 4.Why should !important generally be avoided?

4 left to answer