CSS Selectors: The Complete Reference Guide
Master every CSS selector from basic to advanced: combinators, pseudo-classes, and attribute selectors.
What Are CSS Selectors?
CSS selectors are patterns that tell the browser which HTML elements to style. Every CSS rule you write starts with a selector, and mastering the full range of css selector types is what separates developers who fight with their stylesheets from those who write clean, maintainable CSS.
This guide covers everything from the basics you use every day to advanced selectors like :is(), :where(), and attribute pattern matching that can dramatically simplify your code.
The Six Types of Simple Selectors
Before combining selectors into complex patterns, you need a solid grasp of the building blocks.
Type, Class, and ID Selectors
These are the three selectors you'll use most often:
/* Type selector — targets every <p> element */
p {
color: #333;
line-height: 1.6;
}
/* Class selector — targets elements with class="card" */
.card {
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 1.5rem;
}
/* ID selector — targets the single element with id="main-nav" */
#main-nav {
position: sticky;
top: 0;
background: white;
}
/* Universal selector — targets everything */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
Specificity matters here. IDs score (1,0,0), classes score (0,1,0), and type selectors score (0,0,1). A single ID selector will override any number of class selectors, which is why most style guides recommend you prefer classes over IDs for styling.
Chaining Classes
One common point of confusion: .nav.active and .nav .active are completely different selectors.
/* Targets a single element that has BOTH classes */
.btn.btn--primary {
background: #3b82f6;
color: white;
}
/* Targets .active elements that are DESCENDANTS of .nav */
.nav .active {
font-weight: bold;
color: #3b82f6;
}
CSS Combinators: Targeting Elements by Relationship
Combinators let you target elements based on their position relative to other elements in the DOM. There are four you'll use regularly.
<section class="article">
<h2>Section Title</h2>
<p>First paragraph — direct child.</p>
<div class="callout">
<p>Nested paragraph — descendant, not direct child.</p>
</div>
<p>Second paragraph — adjacent sibling of the first.</p>
<p>Third paragraph — general sibling.</p>
</section>
/* Descendant combinator (space) — any nested <p> */
.article p {
font-size: 1rem;
}
/* Child combinator (>) — only DIRECT child <p> elements */
.article > p {
border-left: 3px solid #3b82f6;
padding-left: 1rem;
}
/* Adjacent sibling (+) — the <p> immediately after another <p> */
p + p {
margin-top: 1.25rem;
}
/* General sibling (~) — ALL following <p> siblings */
h2 ~ p {
color: #555;
}
A practical rule: keep your combinator chains to three or four levels deep. Browsers match selectors right-to-left, so .sidebar .widget ul li a forces the browser to check every <a> on the page before narrowing down. Flatter selectors render faster.
CSS Attribute Selectors
The css attribute selector is one of the most underused tools in CSS. You can match elements not just by attribute presence, but by the value, prefix, suffix, or substring of any attribute.
/* Presence — any <input> with a disabled attribute */
input[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
/* Exact match */
input[type="email"] {
background-image: url("/icons/email.svg");
background-repeat: no-repeat;
background-position: right 0.75rem center;
padding-right: 2.5rem;
}
/* Prefix match (^=) — links that open in a new tab */
a[href^="http"] {
padding-right: 1.2em;
background-image: url("/icons/external.svg");
}
/* Suffix match ($=) — link to a PDF */
a[href$=".pdf"]::after {
content: " (PDF)";
font-size: 0.8em;
color: #888;
}
/* Substring match (*=) — any link containing "github" */
a[href*="github"] {
color: #333;
}
/* Word match (~=) — matches space-separated values */
[data-tags~="featured"] {
outline: 2px solid gold;
}
/* Hyphen match (|=) — matches "en" or "en-US" but not "engineer" */
[lang|="en"] {
quotes: "\201C" "\201D";
}
Attribute selectors have the same specificity as class selectors (0,1,0), so they won't create unexpected conflicts the way IDs can.
CSS Pseudo-Classes: State and Structure
A css pseudo-class selects elements based on state, user interaction, or structural position — things you can't express with a class name alone.
User Interaction Pseudo-Classes
/* Styling interactive states in the correct order (LVHA) */
a:link { color: #3b82f6; }
a:visited { color: #7c3aed; }
a:hover { color: #1d4ed8; text-decoration: underline; }
a:active { color: #1e40af; }
/* Focus visible — better than :focus for keyboard navigation */
button:focus-visible {
outline: 3px solid #3b82f6;
outline-offset: 2px;
}
/* Form validation states */
input:valid { border-color: #22c55e; }
input:invalid { border-color: #ef4444; }
input:disabled { background: #f1f5f9; }
Structural Pseudo-Classes
/* Zebra striping a table */
tr:nth-child(even) { background: #f8fafc; }
tr:nth-child(odd) { background: white; }
/* First and last items */
li:first-child { border-top: none; }
li:last-child { border-bottom: none; }
/* Every third item starting from the first */
li:nth-child(3n+1) { font-weight: bold; }
/* Target elements that are the only child of their parent */
p:only-child {
font-style: italic;
}
/* Negation — style all inputs except submit buttons */
input:not([type="submit"]):not([type="reset"]) {
border: 1px solid #cbd5e1;
border-radius: 4px;
}
Pseudo-Elements
Pseudo-elements create virtual elements you can style without adding HTML:
/* Insert content before and after */
.required-field::before {
content: "* ";
color: #ef4444;
font-weight: bold;
}
blockquote::before {
content: "\201C";
font-size: 4rem;
color: #e2e8f0;
line-height: 0;
vertical-align: -0.4em;
}
/* Style the first line or first letter */
article p:first-of-type::first-letter {
font-size: 3rem;
font-weight: bold;
float: left;
line-height: 0.8;
margin-right: 0.1em;
}
/* Custom text selection */
::selection {
background: #bfdbfe;
color: #1e3a8a;
}
Modern Selectors: :is(), :where(), and :has()
These three selectors from Selectors Level 4 are now widely supported and can dramatically clean up your CSS.
:is() — Cleaner Selector Lists
Instead of repeating long selectors:
/* The old way */
header h1, header h2, header h3,
footer h1, footer h2, footer h3,
main h1, main h2, main h3 {
line-height: 1.2;
}
/* With :is() */
:is(header, footer, main) :is(h1, h2, h3) {
line-height: 1.2;
}
:is() takes the specificity of its most specific argument. So :is(#id, .class) has specificity (1,0,0).
:where() — Zero-Specificity Lists
:where() works identically to :is() but always contributes zero specificity, making it ideal for resets and base styles that you want to be easy to override:
/* Base button styles — easily overridden by any class */
:where(button, [role="button"], input[type="submit"]) {
cursor: pointer;
border: none;
border-radius: 4px;
padding: 0.5rem 1rem;
}
:has() — The Parent Selector
:has() lets you style a parent based on what it contains — something developers have wanted for decades:
/* Style a card differently when it contains an image */
.card:has(img) {
padding-top: 0;
}
/* Style a label when its input is checked */
label:has(input:checked) {
font-weight: bold;
color: #3b82f6;
}
/* Style a form when it has invalid fields */
form:has(input:invalid) .submit-btn {
opacity: 0.5;
pointer-events: none;
}
Specificity Quick Reference
Understanding specificity prevents hours of debugging. Remember the three-column system:
| Selector | Specificity | Example |
|---|---|---|
Universal * |
0,0,0 |
* { box-sizing: border-box; } |
| Type | 0,0,1 |
p { color: #333; } |
| Class, attribute, pseudo-class | 0,1,0 |
.card:hover { ... } |
| ID | 1,0,0 |
#header { ... } |
:where() arguments |
0,0,0 |
:where(#id) still 0,0,0 |
| Inline style | 1,0,0,0 |
style="color:red" |
When two rules conflict, higher specificity wins. When specificity ties, the rule that appears later in the stylesheet wins.
Practical Example: A Complete Component
Here's how all these selector types work together in a real navigation component:
/* Base nav styles — low specificity via :where() */
:where(.primary-nav) {
display: flex;
gap: 0;
list-style: none;
padding: 0;
margin: 0;
}
/* Direct children only */
.primary-nav > li {
position: relative;
}
/* All links inside nav */
.primary-nav a {
display: block;
padding: 0.75rem 1.25rem;
color: #374151;
text-decoration: none;
transition: background 150ms ease;
}
/* Interactive states */
.primary-nav a:hover,
.primary-nav a:focus-visible {
background: #f3f4f6;
color: #111827;
}
/* Active page link */
.primary-nav a[aria-current="page"] {
color: #3b82f6;
font-weight: 600;
border-bottom: 2px solid currentColor;
}
/* External links get an indicator */
.primary-nav a[href^="http"]::after {
content: " ↗";
font-size: 0.75em;
}
/* Dropdown — only when nav item :has() a nested list */
.primary-nav li:has(ul) > a::after {
content: " ▾";
font-size: 0.75em;
}
/* Adjacent dropdown list */
.primary-nav li > a + ul {
display: none;
position: absolute;
top: 100%;
left: 0;
background: white;
box-shadow: 0 4px 12px rgb(0 0 0 / 0.1);
}
.primary-nav li:hover > ul,
.primary-nav li:focus-within > ul {
display: block;
}
Performance Tips
A few guidelines to keep your CSS performant:
- Avoid deep descendant chains.
.sidebar .widget .list .item ais much slower than.sidebar-link. - Prefer class selectors over tag selectors for frequently styled elements.
.btnis faster thanbutton. - Use
:is()and:where()to consolidate long selector lists — they're optimized by modern CSS engines. - Limit universal selectors in hot paths.
* + *is fine for spacing utilities but avoid* * * { ... }.
Before shipping, run your CSS through the CSS Minifier to strip whitespace and comments, and use the CSS Formatter during development to keep complex selector chains readable.
Next Steps
Now that you have a complete picture of CSS selectors, here are some natural places to go deeper:
- CSS Specificity in Depth — Learn exactly how browsers resolve conflicts between competing rules, including
!importantand cascade layers. - CSS Custom Properties — Combine selectors with CSS variables to build dynamic, themeable component systems.
- CSS Grid and Flexbox Layout — Put your selector skills to work targeting layout children with
:nth-child()and:first-of-type.
Tools to use right now:
- CSS Formatter — Paste messy CSS and get properly indented, readable output.
- CSS Minifier — Compress your final stylesheet for production.
The best way to internalize css selectors is to open your browser's DevTools, inspect a real page, and experiment with document.querySelectorAll() in the console — it accepts the same selectors as CSS, so you get instant visual feedback on exactly which elements a selector matches.
Related Tools
CSS Formatter
Beautify and format CSS code with proper indentation and structure.
CSS Minifier
Minify CSS code to reduce file size. Remove comments and unnecessary whitespace.
Text Shadow Generator
Create CSS text shadows with multiple layers. Perfect for headings and effects.
Background Generator
Create CSS background patterns, gradients, and effects for your designs.
Continue Learning
CSS Fundamentals: Styling Web Pages
Master CSS basics including selectors, properties, and the box model.
intermediateCSS Specificity: How the Cascade Really Works
Learn how CSS specificity and the cascade determine which styles apply to your elements.
intermediateCSS Variables (Custom Properties): Complete Guide
Master CSS custom properties for maintainable, dynamic stylesheets.