CSS Basics: Selectors, the Cascade and Specificity
How CSS rules work: adding styles to a page, selectors, properties and values, how the cascade decides which rule wins, specificity, inheritance, and cascade layers.
Three ways to add CSS to a page
HTML describes what the content is; CSS (Cascading Style Sheets) describes how it looks. See What Are HTML and CSS? for how the two fit together. There are three places to put CSS:
-
An external stylesheet: a separate
.cssfile, linked from the<head>with a<link>element. Every page of the site shares it, and the browser caches it, so the second page loads faster. This is the normal choice. -
An internal stylesheet: a
<style>element in the<head>. Useful for a single page or a quick test, but other pages cannot reuse it. -
Inline styles: a
styleattribute on one element. Hard to maintain, impossible to reuse, and they beat almost every other rule (see the cascade). A strict Content Security Policy may also block them.
An external stylesheet on the Kestrel Energy customer portal:
<head>
<meta charset="utf-8">
<title>Meter readings — Kestrel Energy</title>
<link rel="stylesheet" href="css/portal.css">
</head>
The live example below uses an internal stylesheet and one inline style, because an example cannot load a separate file. The Result frame under the code is the page as your browser draws it.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Planned maintenance</title>
<style>
h1 { font-size: 1.5rem; color: darkblue; }
p { color: dimgray; }
</style>
</head>
<body>
<h1>Planned maintenance</h1>
<p>The customer portal is offline on Sunday from 02:00 to 04:00.</p>
<p style="color: darkred;">Meter readings sent in that window are saved later.</p>
</body>
</html>
The anatomy of a rule
A stylesheet is a list of rules. A CSS rule (also called a rule set) has two parts: a selector that says which elements it applies to, and a declaration block in curly braces that says what to change.
/* selector declaration block */
.meter-reading {
color: darkgreen; /* declaration: property "color", value "darkgreen" */
font-weight: bold; /* declarations end with a semicolon */
}
Each line inside the braces is a
CSS declaration: a
property name, a colon, and a
value. Property names are fixed words from the CSS
specifications (color, margin,
font-size); the value must be something that property
accepts. Comments are written /* like this */; CSS has
no // line comments.
CSS is forgiving in a specific way: a declaration the browser does not understand is dropped on its own, and the rest of the rule still applies. That keeps old browsers working when new properties appear, but it also means a typo fails silently.
<p class="reading">Last reading: 4,182 kWh</p>
.reading {
color: darkgreen;
font-size: 20pxx; /* typo: this one declaration is ignored */
font-weight: bold; /* still applied */
}
Selectors
A CSS selector is a pattern that matches elements in the document tree. The same selector syntax is used by JavaScript's querySelector(), so learning it once pays off twice.
| Selector | Matches |
|---|---|
p |
Every <p> element (a
type selector)
|
.notice |
Every element with notice in its
class attribute
|
#outage-map |
The element with id="outage-map" (an
ID selector)
|
* |
Every element (the universal selector) |
[required] |
Elements that have a required attribute |
a[href^="https:"] |
Links whose href starts with
https:
|
p.notice |
<p> elements that also have class
notice (no space: both conditions on one
element)
|
h1, h2, h3 |
All three (a selector list: one rule for several selectors) |
Class and ID selectors
A class selector is a dot
followed by a class name: .notice matches every element
whose class attribute contains the word
notice. An element can have several classes, separated
by spaces (class="notice warning"), and the same class
can be used on any number of elements. Classes are the workhorse of
CSS: they describe a role ("this is a notice") that you can style in
one place.
An ID selector (#notice) matches the one element with
that id. IDs are useful as link targets
(href="#notice") and for connecting a
<label> to its input, but they make poor style
hooks: they cannot be reused, and their high
specificity makes them hard to override.
<nav aria-label="Portal">
<ul class="menu">
<li><a href="#" aria-current="page">Dashboard</a></li>
<li><a href="#">Meter readings</a></li>
<li><a href="#">Invoices</a></li>
</ul>
</nav>
<p class="notice">Your next invoice is ready.</p>
.menu { list-style: none; padding: 0; }
.menu a { color: navy; }
a[aria-current="page"] { font-weight: bold; }
.notice { border-left: 4px solid navy; padding-left: 0.5rem; }
The attribute selector a[aria-current="page"] styles
the current page in the menu using the same attribute that tells
screen reader users which page they are on. Styling the meaning that
is already in the HTML keeps the two in step.
Pseudo-classes
A pseudo-class selects elements by their state or position, something that is not written as an attribute in the HTML. It starts with one colon:
-
User interaction:
:hover(pointer over it),:focus(has keyboard focus),:focus-visible(has focus and the browser decides a focus ring should show, typically after keyboard use),:active(being pressed). -
Links and forms:
:visited,:checked,:disabled,:required,:invalid. -
Position in the tree:
:first-child,:last-child,:nth-child(odd),:nth-child(3n). -
Logic:
:not(.paid),:is(h2, h3),:where(…)and:has(…), covered in Custom Properties and Modern CSS.
<ul class="readings">
<li>January: 312 kWh</li>
<li>February: 288 kWh</li>
<li>March: 251 kWh</li>
<li>April: 197 kWh</li>
</ul>
<p><a href="#" class="download">Download as CSV</a></p>
.readings li:nth-child(odd) { background: whitesmoke; }
.readings li:first-child { font-weight: bold; }
.download:hover { text-decoration: none; }
.download:focus-visible { outline: 3px solid navy; outline-offset: 2px; }
Hover over the link, then click inside the result frame and press Tab to see the focus style. Never remove the focus outline without putting a clearly visible one back: keyboard users depend on it (see Accessibility Basics).
Pseudo-elements
A pseudo-element selects a
part of an element, or creates a box that is not in
the HTML at all. It starts with two colons:
::before and ::after insert generated
content (they need a content property),
::first-line and ::first-letter style part
of the text, ::marker styles list bullets and numbers,
and ::placeholder styles the hint text in an input.
<p class="tip">Read your meter at the same time each month, so the
numbers are easy to compare. The portal draws a chart of your usage
once you have entered three readings.</p>
<ul class="steps">
<li>Find the meter</li>
<li>Write down the number</li>
<li>Enter it in the portal</li>
</ul>
.tip::before { content: "Tip: "; font-weight: bold; color: darkgreen; }
.tip::first-line { text-transform: uppercase; }
.steps li::marker { color: darkgreen; }
Combinators
A combinator joins two selectors and describes how the elements are related in the tree:
| Combinator | Example | Matches |
|---|---|---|
| Descendant (a space) | .outage p |
Every p anywhere inside .outage
|
Child > |
.outage > p |
Only p elements that are direct children of
.outage
|
Next sibling + |
h2 + p |
The p immediately after an h2,
with the same parent
|
Subsequent sibling ~ |
h2 ~ footer |
Every footer after an h2, with the
same parent
|
<article class="outage">
<h2>Outage in the Riverside district</h2>
<p>Reported at 14:05. About 1,200 addresses are affected.</p>
<p>Engineers are on site.</p>
<footer>
<p>Last updated 14:40</p>
</footer>
</article>
.outage p { margin: 0.25rem 0; } /* all four p elements */
.outage > p { color: darkslategray; } /* not the p in the footer */
h2 + p { font-weight: bold; } /* only the first p */
h2 ~ footer { font-size: 0.875rem; color: dimgray; }
Selectors are read by the browser from
right to left: for .outage p it takes
each p and walks up to look for a
.outage ancestor. You rarely need to think about speed,
but long chains like body main article div ul li a are
fragile: any change in the HTML breaks them. Prefer one or two
levels and a class.
The cascade: which rule wins
Often several rules set the same property on the same element. The cascade is the algorithm that decides which declaration wins. It is the "C" in CSS. The browser collects every declaration that applies, then compares them on the criteria below, in this order. The first criterion where they differ decides; later criteria only matter when all earlier ones are equal.
Origin and importance
Declarations come from three origins: the browser's
built-in stylesheet (the user agent styles that make
headings big and links blue), the user (settings such as a minimum
font size, or a user stylesheet), and the author (you, the site's
stylesheets). Normal author styles beat user styles, which beat
browser defaults. That is why your
h1 { font-size: 1.5rem; } simply replaces the browser's
heading size.
Order of appearance
When everything else is equal, the declaration that comes last wins: later in the same stylesheet, or in a stylesheet linked later. What counts is the order of the rules in the CSS, not the order of the class names in the HTML.
<p class="notice warning">Payment overdue</p>
.warning { background: lightyellow; }
.notice { background: lightblue; } /* same specificity, later: wins */
The background is light blue. Writing
class="warning notice" instead would change nothing.
(CSS Cascading Level 6 adds one more step for
@scope rules: scope proximity, between specificity and
order of appearance.)
Specificity
Specificity is a weight the browser calculates for each selector. When two declarations come from the same origin and layer, the one with the more specific selector wins, wherever it appears in the stylesheet. Specificity is written as three numbers (A, B, C):
- A: the number of ID selectors.
- B: the number of class selectors, attribute selectors and pseudo-classes.
- C: the number of type selectors and pseudo-elements.
The universal selector * and the combinators count for
nothing. The numbers are compared from left to right, like version
numbers: the first column that differs decides. They do not "carry
over": eleven classes (0, 11, 0) still lose to one ID
(1, 0, 0).
| Selector | Specificity | Why |
|---|---|---|
* |
(0, 0, 0) | The universal selector counts for nothing |
li |
(0, 0, 1) | One type |
ul li a |
(0, 0, 3) | Three types |
li::marker |
(0, 0, 2) | A type and a pseudo-element |
.notice |
(0, 1, 0) | One class: beats ul li a |
nav a:hover |
(0, 1, 2) | A pseudo-class and two types |
a[aria-current="page"] |
(0, 1, 1) | An attribute selector counts like a class |
#notice |
(1, 0, 0) | One ID: beats any number of classes |
:is(#notice, .tip) p |
(1, 0, 1) |
:is(), :not() and
:has() take the specificity of their most
specific argument
|
:where(#notice, .tip) p |
(0, 0, 1) | :where() always counts as zero |
In the example below the ID rule wins, even though two rules come after it:
<p id="status" class="status">Meter reading received</p>
#status { color: darkgreen; } /* (1, 0, 0): wins */
.status { color: darkred; } /* (0, 1, 0) */
p { color: navy; } /* (0, 0, 1) */
Inline style attributes are not part of the specificity
count. They are their own step in the cascade, above layers and
specificity, so a style attribute beats every normal
rule in a stylesheet.
!important
Adding !important after a value moves that declaration into a higher-priority group of the cascade: it beats every normal declaration, including inline styles, whatever their specificity.
<p class="renewal" style="color: navy;">Your contract renews on 1 March.</p>
.renewal { color: darkred !important; } /* beats the inline style */
For important declarations the origin order is reversed: an important user style beats an important author style, which beats an important browser style. That reversal is deliberate. It lets a person with low vision force larger text or higher contrast, whatever the site says.
The problem with !important in your own stylesheets is
that the only way to override it is another
!important with higher specificity, later in the file,
or in an earlier cascade layer. Each one makes the next fight
harder. Reasonable uses are rare: utility classes that must always
win (such as a .visually-hidden class), or overriding
inline styles from third-party code you cannot change.
Inheritance
Some properties are passed from a parent element to its children
when the children have no value of their own. This is
CSS inheritance (not the same
as
inheritance between classes
in programming). As a rule of thumb,
text properties inherit (color,
font-family, font-size,
line-height, text-align), while
box properties do not (margin,
padding, border, width,
background). That is why setting
font-family once on body changes the whole
page, while a border on a section does not appear around every
paragraph inside it.
<section class="card">
<h2>Monthly usage</h2>
<p>You used <strong>251 kWh</strong> in March.</p>
<button type="button">Show details</button>
</section>
.card {
color: darkslategray; /* inherited by h2, p and strong */
font-family: Georgia, serif; /* inherited, except by the button */
border: 1px solid gray; /* not inherited: only the section */
padding: 1rem;
}
Notice the button: form controls get their font from the browser
stylesheet, and that value beats inheritance (an inherited value
only applies when no rule sets the property on the element itself).
The usual fix is
button, input, select, textarea { font: inherit; }.
Four keywords work on every property and control this explicitly:
-
inherit: take the parent's value, even for a property that normally does not inherit. -
initial: use the property's initial value from the specification (fordisplaythat isinline, even on adiv). -
unset: act likeinheritfor inherited properties and likeinitialfor the rest. -
revert: go back to what the browser's own stylesheet (or the user's) would give, for example the normal block display of adiv.
Cascade layers
Large stylesheets mix several kinds of CSS: a reset, base styles for
plain elements, components, utilities, and third-party styles.
Specificity fights between them are common. A
cascade layer, declared with
@layer, puts rules into a named group, and the cascade
compares layers before specificity. A rule in a
later layer beats a rule in an earlier layer, however specific the
earlier one is. Cascade layers have been Baseline widely available
since September 2024.
<p id="banner" class="alert">Read-only mode until 06:00.</p>
<p class="alert note">Changes made now are not saved.</p>
/* Declare the order once: later layers win */
@layer base, components;
@layer base {
#banner { color: navy; } /* (1, 0, 0), but in the earlier layer */
}
@layer components {
.alert { color: darkred; border: 2px solid; padding: 0.5rem; }
}
.note { color: black; } /* not in a layer: beats every layer */
The result shows both rules of layers:
-
The first paragraph is dark red:
componentsis declared afterbase, so.alertbeats#bannerdespite its lower specificity. - The second paragraph is black: styles that are not in any layer beat all layered styles. (The border is black too, because a border without a color uses the text color.)
Within one layer, specificity and order of appearance work as
before. For !important declarations the layer order is
reversed, just like the origins: an important declaration in the
first layer wins. You can also load a whole file into a
layer: @import url("reset.css") layer(base);.