1CSS Selectors
▶
CSS selectors are the mechanism by which stylesheets identify which HTML elements should receive a given set of style rules. Every CSS rule begins with a selector — a pattern that the browser evaluates against the document tree to find matching elements. Understanding selectors deeply is essential because they control not only what gets styled, but also how confidently a rule wins when multiple rules compete for the same element. This topic covers the main selector types, the ways they can be combined, and the twin concepts of specificity and the cascade that govern how conflicts are resolved.
Element selectors are the most fundamental building block of CSS. They are written simply as the name of an HTML tag with no prefix, and they match every instance of that element anywhere in the document.
p {
font-size: 1rem;
line-height: 1.6;
}
h1 {
font-size: 2.5rem;
font-weight: bold;
}
a {
color: steelblue;
}
In the example above, every <p> element on the page receives a font size of 1rem and a line height of 1.6. Every <h1> receives its own sizing, and every anchor tag is coloured steelblue. Because element selectors cast such a wide net, they are ideal for establishing sensible global defaults — the baseline appearance that everything else builds on. The trade-off is that this breadth means they are easy to accidentally override; element selectors sit at the lowest rung of specificity among the principal selector categories. Any class rule or ID rule targeting the same element will beat them.
A practical pattern is to use element selectors for typographic and layout resets at the top of a stylesheet, then layer more targeted rules on top:
/* Base reset with element selectors */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', sans-serif;
background-color: #f9f9f9;
}
Class selectors are written in CSS with a leading period (.) followed by the class name, and they match any HTML element whose class attribute includes that name. Unlike element selectors, class selectors are opt-in — an element receives the style only when you deliberately add the class to its markup.
/* CSS */
.highlight {
background-color: yellow;
font-weight: bold;
}
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
}
<!-- HTML -->
<p class="highlight">This paragraph is highlighted.</p>
<div class="card">This div looks like a card.</div>
<span class="highlight">This span is also highlighted.</span>
Notice that .highlight is applied to both a <p> and a <span>. This reusability across different element types is one of the defining strengths of class selectors — the same visual treatment can be applied consistently regardless of which tag is involved.
An HTML element can carry multiple classes at once, separated by spaces in the class attribute. Each class contributes its own block of declarations, and they stack together:
<div class="card highlight">
This div gets card styles AND highlight styles.
</div>
This composable nature makes class selectors the backbone of most modern CSS methodologies such as BEM (Block Element Modifier) and utility-first frameworks like Tailwind CSS, where dozens of small, single-purpose classes are combined to produce complex visual results.
ID selectors are written with a leading hash (#) followed by the identifier, and they target the element whose id attribute matches that value. The critical rule of HTML is that every id must be unique within a document — no two elements should share the same ID. This means an ID selector will match at most one element on the page at any given time.
/* CSS */
#main-header {
background-color: #222;
color: #fff;
padding: 1rem 2rem;
}
#submit-btn {
background-color: #0070f3;
color: white;
border: none;
padding: 0.75rem 1.5rem;
cursor: pointer;
}
<!-- HTML -->
<header id="main-header">My Website</header>
<button id="submit-btn">Submit</button>
The practical implication of uniqueness is that ID selectors are best reserved for landmark elements — things like a site header, a navigation bar, a main content region, or a modal — where there genuinely is only ever one instance. Because IDs are also used by JavaScript's getElementById and as anchor link targets (href="#section"), overloading them purely for styling can create coupling between concerns.
ID selectors carry significantly higher specificity than class or element selectors. This means an ID rule will override a class rule or element rule targeting the same element, even if the class or element rule appears later in the stylesheet. This power can be a double-edged sword — while useful, it can make styles difficult to override later without resorting to even more specific selectors or !important.
Combination selectors let you express more nuanced relationships between elements. Rather than targeting elements in isolation, combination selectors describe structural or attributive conditions that must be satisfied simultaneously.
A descendant selector is written as two (or more) selectors separated by a space. It matches any element that satisfies the right-hand selector and is a descendant (at any depth) of an element satisfying the left-hand selector:
/* Targets any <a> that lives anywhere inside a <nav> */
nav a {
text-decoration: none;
color: #333;
}
/* Targets any <li> inside a <ul> with class "menu" */
ul.menu li {
list-style: none;
padding: 0.5rem 0;
}
A child selector uses the > combinator and is more restrictive — it matches only direct children, not deeper descendants. This precision is valuable when you have nested structures and only want to style the immediate children:
/* Only direct <li> children of this <ul>, not nested <li>s inside nested <ul>s */
ul.top-level > li {
font-weight: bold;
}
/* Only <p> elements that are direct children of .article */
.article > p {
margin-bottom: 1rem;
}
Grouped selectors allow you to apply the same declaration block to multiple selectors by separating them with commas. This avoids repeating identical rules and keeps the stylesheet DRY (Don't Repeat Yourself):
/* Instead of three separate rules with identical declarations... */
h1, h2, h3 {
font-family: 'Georgia', serif;
color: #111;
margin-bottom: 0.5em;
}
/* Also works with mixed selector types */
.error, #form-alert, span.warning {
color: red;
border: 1px solid red;
padding: 0.25rem 0.5rem;
}
You can also chain class and element selectors (with no space between them) to narrow targeting to elements that match both conditions simultaneously. For instance, p.intro targets only <p> elements that also have the class intro — a plain <div class="intro"> would not match:
/* Only <p> tags with class "intro" — not <div class="intro"> */
p.intro {
font-size: 1.2rem;
color: #555;
}
/* An <a> that has BOTH "nav-link" and "active" classes */
a.nav-link.active {
color: #0070f3;
border-bottom: 2px solid #0070f3;
}
CSS specificity is a scoring system the browser uses to decide which rule wins when two or more rules target the same element and set the same property. Each selector type contributes a different number of points to a rule's specificity score. The browser compares scores from most significant to least significant digit, not as simple integers.
| Selector Type | Specificity Notation | Example | Score (A, B, C) |
|---|---|---|---|
| Inline style | (1, 0, 0, 0) | style="color: red;" |
Highest |
| ID selector | (0, 1, 0, 0) | #main-header |
High |
| Class selector | (0, 0, 1, 0) | .highlight |
Medium |
| Attribute selector | (0, 0, 1, 0) | [type="text"] |
Medium |
| Pseudo-class | (0, 0, 1, 0) | :hover |
Medium |
| Element selector | (0, 0, 0, 1) | p |
Low |
| Universal selector | (0, 0, 0, 0) | * |
No specificity |
When multiple selectors are combined, their specificity scores are added together. Consider the following competing rules for a paragraph inside a div that has an ID:
#content p.intro {
color: green; /* specificity: 0,1,1,1 */
}
.wrapper p {
color: blue; /* specificity: 0,0,1,1 */
}
p {
color: red; /* specificity: 0,0,0,1 */
}
For a <p class="intro"> inside <div id="content">, the first rule wins with a score of (0,1,1,1), regardless of where the rules appear in the file. The second rule beats the third for any other paragraph inside .wrapper. The third rule only wins when none of the more specific selectors match.
A crucial tiebreaker: when two rules have exactly equal specificity, the one that appears later in the stylesheet wins. This is called source order, and it is the final differentiator when everything else is equal:
.btn {
background-color: grey;
}
.btn {
background-color: blue; /* This wins — same specificity, declared later */
}
A key best practice is to keep specificity as low and consistent as possible. Deeply nested selectors like div#sidebar ul.nav li a.active span become extremely difficult to override without writing equally or more specific rules, which quickly leads to an arms race of ever-escalating specificity that makes the stylesheet fragile and hard to maintain. Preferring flat, class-based selectors keeps the stylesheet flexible and predictable.
The cascade is the algorithm CSS uses to resolve conflicts between all style rules that could apply to an element. The word "cascading" in CSS is a direct reference to this process — rules flow down through several levels of evaluation before a winner is declared.
The cascade evaluates rules in the following order of origin and importance:
- Browser default styles (user-agent stylesheet) — every browser ships with built-in styles for HTML elements (e.g., blue underlined links, bold headings). These have the lowest priority.
- User styles — some browsers allow users to apply their own stylesheets (for accessibility reasons, for example). These override browser defaults.
- Author styles — the stylesheets written by the developer. These override user and browser styles under normal conditions.
- Author
!importantstyles — author rules marked!importantleap ahead in priority. - User
!importantstyles — user rules marked!importantoverride even author!importantrules (this protects accessibility overrides).
Within the same origin and importance level, specificity is then compared, and within equal specificity, source order determines the winner — the later rule prevails.
The !important declaration overrides the normal cascade:
p {
color: red !important; /* Will beat a more specific class rule */
}
.intro {
color: blue; /* Loses to the !important rule above */
}
While !important can seem like a convenient escape hatch, it comes at a significant cost to maintainability. Once a property is declared !important, overriding it later requires either another !important (which restarts the arms race) or restructuring the stylesheet. It also makes it much harder to trace why a style is being applied, since it breaks the expected mental model of specificity and source order. Legitimate uses of !important tend to be limited to utility classes (like a .visually-hidden accessibility class) or third-party stylesheet overrides where you have no control over the source.
Putting it all together, consider this scenario:
/* Author stylesheet */
body {
font-size: 16px; /* element selector — low specificity */
}
.content p {
font-size: 14px; /* class + element — medium specificity */
}
#article p {
font-size: 18px; /* ID + element — high specificity */
}
<div id="article" class="content">
<p>What size am I?</p>
</div>
The <p> here is targeted by all three rules. The cascade first checks origin (all are author styles — tied). It then checks specificity: #article p scores (0,1,0,1), .content p scores (0,0,1,1), and body scores (0,0,0,1). The ID selector wins, so the paragraph renders at 18px. Understanding this evaluation pipeline — origin → specificity → source order — is what allows developers to write CSS that behaves predictably rather than feeling like a guessing game.