Skip to content
C Codeloom
HTML & CSS

CSS Selectors and Specificity Explained

Selectors decide which elements get styled; specificity decides which rule wins when more than one applies. This guide covers every selector you need and how to read a specificity score.

·9 min read · By Yash Kesharwani
Intermediate 11 min read

What you'll learn

  • Element, class, id, and attribute selectors
  • Pseudo-classes and pseudo-elements
  • Combinators: descendant, child, adjacent sibling, general sibling
  • How specificity is calculated and why it matters
  • When to reach for !important — and why almost never
  • How the cascade picks a winner when rules conflict

Prerequisites

A CSS rule has two parts: a selector that picks which elements to style, and a block of declarations that say how. Selectors are how you connect your stylesheet to your HTML. Specificity is how the browser decides which rule wins when more than one targets the same element.

Almost every confusing CSS bug — “why isn’t my style applying?” — comes down to these two ideas. This post covers them properly.

The basic selectors

Five selectors do most of the work in a real stylesheet.

Element selector — targets every tag of a given name:

p {
  line-height: 1.6;
}

Class selector — targets every element with the given class attribute. Prefixed with a dot:

.card {
  border: 1px solid #ddd;
}

ID selector — targets the single element with the given id. Prefixed with #:

#site-header {
  background: black;
}

Attribute selector — targets elements based on attributes and their values:

input[type="email"] {
  border-color: steelblue;
}

a[href^="https://"] {
  /* links whose href starts with https:// */
}

img[alt=""] {
  /* images with empty alt text */
}

The operators inside the brackets are worth knowing:

  • [attr="value"] — exact match
  • [attr^="value"] — starts with
  • [attr$="value"] — ends with
  • [attr*="value"] — contains

Universal selector — targets everything. Use sparingly:

* {
  box-sizing: border-box;
}

Pseudo-classes

A pseudo-class targets an element in a particular state. The syntax is a colon followed by a name.

a:hover {
  text-decoration: underline;
}

a:visited {
  color: purple;
}

input:focus {
  outline: 2px solid steelblue;
}

button:disabled {
  opacity: 0.5;
}

li:first-child {
  font-weight: bold;
}

li:last-child {
  border-bottom: none;
}

li:nth-child(odd) {
  background: #f7f7f7;
}

The structural pseudo-classes — :first-child, :last-child, :nth-child() — are how you style “every other row” or “the first item in a list” without adding extra classes.

A few modern pseudo-classes worth knowing:

  • :not(selector) — anything that does not match the given selector.
  • :is(a, b, c) — matches any of a list of selectors. Lower-specificity grouping.
  • :where(a, b, c) — like :is, but contributes zero specificity. Great for low-specificity resets.
  • :has(selector) — the long-awaited “parent” selector. Matches elements that contain something matching the inner selector.
/* Card that contains an image — style it differently */
.card:has(img) {
  padding-top: 0;
}

Pseudo-elements

A pseudo-element targets a part of an element rather than the whole. The syntax is two colons followed by a name.

p::first-line {
  font-weight: bold;
}

p::first-letter {
  font-size: 2em;
}

.button::before {
  content: "→ ";
}

.button::after {
  content: " ←";
}

::selection {
  background: yellow;
}

::before and ::after are the workhorses. They create a virtual element inside the target, controlled entirely by CSS — perfect for icons, dividers, and decorative flourishes. The content property is required for them to render at all.

Combinators

Combinators let you target elements based on their relationship to others.

Descendant combinator (space) — any matching descendant, at any depth:

article p {
  /* every <p> inside an <article>, no matter how deeply nested */
}

Child combinator (>) — direct children only:

ul > li {
  /* only <li> that are direct children of a <ul> */
}

Adjacent sibling combinator (+) — the element immediately after another:

h2 + p {
  /* the <p> that comes right after an <h2> */
  margin-top: 0;
}

General sibling combinator (~) — every following sibling:

h2 ~ p {
  /* every <p> that follows an <h2> within the same parent */
}

The difference between descendant and child matters more than beginners expect. nav a selects every link inside a <nav>, even deeply nested ones. nav > a selects only direct children. Pick deliberately.

Try it yourself. Create a list with five items. Use li:nth-child(odd) to give odd items a light background. Then use li + li to give every list item except the first a top border. The combination produces nicely separated stripes.

Specificity: which rule wins

When two rules both apply to an element and they set the same property, the browser has to pick one. The rule with higher specificity wins.

Specificity is a four-part score, often written a, b, c, d:

  • a — inline styles (the style="" attribute). Almost always 0.
  • b — id selectors.
  • c — classes, attribute selectors, and pseudo-classes.
  • d — element selectors and pseudo-elements.

Higher numbers in higher slots beat any number in lower slots. Some examples:

SelectorSpecificity
p0, 0, 0, 1
.card0, 0, 1, 0
#header0, 1, 0, 0
ul li0, 0, 0, 2
.nav a:hover0, 0, 2, 1
#main .card p0, 1, 1, 1
input[type="email"]0, 0, 1, 1
style="..." (inline)1, 0, 0, 0

A concrete conflict:

.card p {
  color: gray; /* specificity: 0, 0, 1, 1 */
}

p {
  color: black; /* specificity: 0, 0, 0, 1 */
}

A <p> inside a .card ends up gray. The first rule wins because its specificity is higher.

If two rules have the same specificity, the one written later in the stylesheet wins. That is the cascade tiebreaker.

A practical reading of specificity

You almost never need to count specificity precisely. Most of the time the question is just: “is my selector specific enough to beat the other one?” A workflow:

  1. Open developer tools and click the element.
  2. Look at the Styles panel. The browser shows every rule that applies, with the winners on top and crossed-out losers below.
  3. If your rule is being overridden, you can see exactly which one is beating it.
  4. Make your selector slightly more specific — add a class — or move your rule lower in the cascade.

That loop fixes 99% of “my styles aren’t applying” problems.

!important — and why to avoid it

Adding !important to a declaration overrides everything except another !important of the same specificity:

p {
  color: red !important;
}

This wins over almost any other rule for the same property. It also makes the rest of your stylesheet very hard to maintain, because the only way to override an !important rule is with another !important rule. Over a few months, an !important-heavy stylesheet becomes a stack of overrides arguing with each other.

Reasonable uses of !important:

  • Utility classes designed to override anything (!important is intentional there).
  • Overriding styles from a library you cannot edit.
  • Debugging in DevTools to confirm what a rule would do.

If you are reaching for !important to fix your own CSS, that is a signal — restructure the selector or the cascade instead.

The cascade and origin

When multiple rules target the same element, the browser decides the winner in this order:

  1. Origin and importance. User agent (browser defaults) → author (your CSS) → user (browser settings) — with !important flipping some of these around.
  2. Specificity. Highest wins.
  3. Source order. The rule written later wins.

For your day-to-day work, you can usually shrink this to “specificity, then source order.” Origin only matters when you start inheriting from frameworks or fighting browser defaults.

Try it yourself. Write two rules targeting the same <p> — one with selector p and one with selector .note. Give them different colors. Open DevTools and switch the order in the stylesheet. Notice how source order only matters when specificity ties.

Practical guidelines

A few habits that prevent specificity headaches before they start:

  • Lean on classes. They are flexible, reusable, and have the right level of specificity for most styling. Reserve id for JavaScript hooks and in-page anchors.
  • Avoid deep selector chains. nav.main ul li a span is brittle. Style with a single class on the thing you want to target.
  • Use :where() for resets. It has zero specificity, so your real rules will always beat it.
  • Inspect first, edit second. Open DevTools. Read the Styles panel. Find the rule that is winning before you change anything.
  • Save !important for emergencies — and even then, leave a comment explaining why.

Recap

You now know:

  • The basic selectors: element, class, id, attribute, universal
  • Pseudo-classes target states (:hover, :focus, :nth-child)
  • Pseudo-elements target parts of an element (::before, ::after, ::first-line)
  • Combinators describe relationships: descendant, child (>), adjacent sibling (+), general sibling (~)
  • Specificity is a four-part score; higher wins, ties broken by source order
  • !important wins almost everything but corrodes maintainability
  • The cascade resolves conflicts using origin, specificity, and source order

Next steps

You can now select and style elements with confidence. The next step is making sure those styles look right on every screen — from a phone in portrait mode to an ultra-wide monitor. That is what responsive design is for.

Next: Responsive Web Design: The Beginner’s Foundation

Questions or feedback? Email codeloomdevv@gmail.com.