HTML & CSS · Lesson 6 of 13

Selectors, Specificity and the Cascade

Master CSS selectors, pseudo-classes and pseudo-elements, calculate specificity, and understand the cascade, inheritance, :is(), :where() and @layer.

  • Beginner
  • 17 min read
  • 4 objectives

Before this lessonLesson 5: CSS Basics

What you will learn

  • Target elements precisely with combinators, attribute selectors and pseudo-classes
  • Calculate specificity and predict which rule wins
  • Understand inheritance and the keywords inherit, initial and unset
  • Use :is(), :where(), :has() and @layer to keep CSS manageable

Your Progress

0 of 13 lessons 0%

  • Lessons0 / 13
  • Completed0
  • Est. time left~ 4 hours

Create a free account to keep your progress on every device.

Tip: pressing Next marks this lesson complete automatically.

In CSS Basics you met element, class and ID selectors and heard that "the more specific rule wins". That is enough for a small page. On a real site, with hundreds of rules, you will eventually write a style that simply refuses to apply. This lesson explains exactly how the browser decides, so you can fix those problems in seconds instead of reaching for !important.

Combinators: selecting by relationship

Combinators select elements based on where they sit in the HTML tree.

/* Descendant (space): any <a> anywhere inside .nav */
.nav a { color: #1e293b; }

/* Child (>): only <li> that are direct children of .menu */
.menu > li { display: inline-block; }

/* Adjacent sibling (+): the <p> immediately after an <h2> */
h2 + p { margin-top: 0; font-size: 1.125rem; }

/* General sibling (~): every <p> after an <h2>, same parent */
h2 ~ p { color: #475569; }

/* Grouping (,): same rules for several selectors */
h1, h2, h3 { line-height: 1.2; }

Attribute selectors

input[type="email"] { background: #f8fafc; }
a[href^="https://"] { padding-right: 1em; }     /* starts with */
a[href$=".pdf"]::after { content: " (PDF)"; }    /* ends with */
[class*="btn-"] { cursor: pointer; }             /* contains */
[disabled] { opacity: 0.5; }                     /* attribute present */

Pseudo-classes and pseudo-elements

A pseudo-class (one colon) selects an element in a particular state or position. A pseudo-element (two colons) styles a part of an element, or inserts generated content.

/* States */
a:hover { text-decoration: underline; }
button:focus-visible { outline: 3px solid #2563eb; }
input:invalid { border-color: #dc2626; }
input:checked + label { font-weight: 600; }

/* Position */
li:first-child { border-top: none; }
li:last-child { border-bottom: none; }
tr:nth-child(even) { background: #f1f5f9; }   /* zebra stripes */
li:nth-child(3n + 1) { clear: left; }          /* 1st, 4th, 7th... */
p:not(.lead) { font-size: 1rem; }

/* Parts and generated content */
p::first-line { font-weight: 600; }
.required::after { content: " *"; color: #dc2626; }
::selection { background: #fde68a; }
input::placeholder { color: #94a3b8; }

The :has() parent selector

For decades CSS could not style a parent based on its children. :has(), supported in every major browser since late 2023, changes that. It reads as "an element that has...".

/* A card that contains an image gets a different layout */
.card:has(img) { display: grid; grid-template-columns: 120px 1fr; }

/* Highlight a form field wrapper when its input is invalid */
.field:has(input:invalid) label { color: #dc2626; }

/* Style a label whose checkbox is checked */
label:has(input[type="checkbox"]:checked) { background: #ecfdf5; }

Specificity: how the browser scores selectors

When two rules set the same property on the same element, the browser compares their specificity. Think of it as a three-part score written (IDs, classes, elements):

  • A: count of ID selectors (#header).
  • B: count of classes, attribute selectors and pseudo-classes (.btn, [type], :hover).
  • C: count of element selectors and pseudo-elements (div, ::before).

Compare column by column from the left. Any ID beats any number of classes, just as 1-0-0 is bigger than 0-15-0.

p                     /* (0,0,1) */
.intro                /* (0,1,0) */
p.intro               /* (0,1,1) */
.card .title          /* (0,2,0) */
nav ul li a:hover     /* (0,1,4) */
#hero .title          /* (1,1,0)  wins over all of the above */
*                     /* (0,0,0)  the universal selector adds nothing */

Inline style="..." attributes beat every selector, and a declaration marked !important beats even inline styles. If two rules tie on specificity, the one that appears later in the CSS wins.

:is() and :where()

Both take a list of selectors and match if any of them match, which shortens repetitive CSS. The difference is specificity: :is() takes the specificity of its most specific argument, while :where() always counts as zero. That makes :where() perfect for defaults that should be easy to override.

/* Instead of: article h1, article h2, article h3 { ... } */
article :is(h1, h2, h3) { margin-top: 2rem; }

/* Zero-specificity reset: any single class will override it */
:where(ul, ol)[role="list"] { list-style: none; padding: 0; }

Inheritance

Some properties pass automatically from parent to child. Mostly these are text properties: color, font-family, font-size, line-height. Box properties such as margin, padding, border and background do not inherit. That is why you set fonts once on body and every paragraph picks them up.

body { font-family: system-ui, sans-serif; color: #1e293b; }

/* Buttons and inputs do NOT inherit fonts by default; force it */
button, input, select, textarea { font: inherit; color: inherit; }

.reset-me { all: unset; }   /* wipe every property back to inherited/initial */

The keywords: inherit copies the parent's value, initial uses the CSS default for that property, and unset acts as inherit for inherited properties and initial for the rest.

Cascade layers with @layer

Cascade layers let you decide which group of styles wins before specificity is even compared. Layers declared later win, regardless of how specific the selectors inside earlier layers are. This is a clean way to keep resets, third-party CSS and your components from fighting.

@layer reset, base, components, utilities;

@layer reset {
  * { margin: 0; box-sizing: border-box; }
}
@layer components {
  #app .card .title { color: #334155; }   /* high specificity... */
}
@layer utilities {
  .text-brand { color: #2563eb; }         /* ...still loses to this later layer */
}

The full order the browser checks is: origin and !important, then layers, then specificity, then source order. In DevTools, the Styles panel lists rules in that order and strikes through the losers.

Recap

  • Combinators (space, >, +, ~) and attribute selectors target elements by relationship and attributes.
  • Pseudo-classes select states (:hover, :nth-child, :has()); pseudo-elements style parts (::before, ::placeholder).
  • Specificity is scored (IDs, classes, elements) and compared left to right; ties go to the later rule.
  • Text properties inherit, box properties do not; use font: inherit on form controls.
  • Keep specificity low with classes and :where(), and organise big codebases with @layer.
// Write your solution here

Finished reading? Mark this lesson complete to track your progress.

Up next · Lesson 7The Box Model, Sizing and UnitsUnderstand the CSS box model in depth: margins, padding, borders, box-sizing, display types, margin collapse, min/max sizing and units like rem, vw and ch.