CSS Selectors Deep Dive: From Basic to :has() & :is()
Master CSS selectors from basic to advanced. Covers combinators, attribute selectors, pseudo-classes, modern selectors (:is, :where, :not, :has), specificity scoring, nth-child patterns, pseudo-elements, and practical selector patterns for real-world styling.
- 1. CSS Flexbox Mastery: The Complete Visual Guide to Flexible Layouts
- 2. CSS Grid Mastery: From Basics to Advanced Layouts
- 3. CSS Custom Properties (Variables) Mastery: Dynamic Theming & Beyond
- 4. CSS Animations & Transitions Mastery: From Hover Effects to Keyframe Sequences
- 5. CSS Selectors Deep Dive: From Basic to :has() & :is()
- 6. CSS Box Model Mastery: Content, Padding, Border & Margin Explained
- 7. CSS Responsive Design Mastery: Mobile-First, clamp(), & Container Queries
- 8. CSS Specificity & Cascade Mastery: How Browsers Resolve Conflicts
- 9. CSS Modern Layout Techniques: Scroll Snap, Subgrid, Logical Properties & More
- 10. CSS Pseudo-Elements Mastery: ::before, ::after, Counters & Decorative Techniques
CSS selectors are how you tell the browser which elements to style. They're the targeting system of CSS — from simple element names to complex combinators and modern pseudo-classes like :has() that can select parent elements.
Understanding selectors deeply is essential for writing clean, efficient CSS. In this guide, we'll start with the basics and work our way up to modern selectors that are changing how we write CSS entirely.
1. Basic Selectors
Universal Selector (*)
Matches every element on the page:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
Type Selector
Matches all elements of a specific HTML tag:
p { color: #334155; }
h1 { font-size: 2rem; }
a { text-decoration: none; }
Class Selector (.)
Matches elements with a specific class attribute:
.card { padding: 1rem; }
.btn-primary { background: blue; }
ID Selector (#)
Matches the single element with a specific ID:
#hero { height: 100vh; }
#main-nav { position: sticky; }
Best Practice: Avoid using IDs for styling. Use classes instead. IDs have very high specificity and can cause cascading issues. Save IDs for JavaScript hooks and anchor links.
2. Combinator Selectors
Combinators define the relationship between elements.
Descendant Combinator (space)
Matches any element nested inside another, at any depth:
.card p { color: #64748b; }
/* Styles ALL paragraphs inside .card, no matter how deep */
Child Combinator (>)
Matches only direct children — not grandchildren:
.nav > li { display: inline-block; }
/* Only the direct <li> children of .nav */
Adjacent Sibling (+)
Matches the next sibling immediately following an element:
h2 + p { font-size: 1.1rem; }
/* Only the first <p> directly after each <h2> */
General Sibling (~)
Matches all siblings that follow an element:
h2 ~ p { color: #475569; }
/* ALL paragraphs after an <h2>, at the same level */
3. Attribute Selectors
Target elements based on their HTML attributes:
| Selector | Matches |
|---|---|
[href] | Elements that have an href attribute |
[type="email"] | Elements where type exactly equals "email" |
[class~="btn"] | Elements where class contains the word "btn" |
[href^="https"] | href starts with "https" |
[href$=".pdf"] | href ends with ".pdf" |
[href*="google"] | href contains "google" anywhere |
[data-theme="dark" i] | Case-insensitive match (the i flag) |
/* Style external links differently */
a[href^="https://"] {
color: #3b82f6;
}
/* PDF download links */
a[href$=".pdf"]::after {
content: " 📄";
}
/* Required form fields */
input[required] {
border-color: #ef4444;
}
4. Pseudo-Classes: State-Based Selectors
Pseudo-classes select elements based on their state or position.
User Interaction States
| Pseudo-class | When it matches |
|---|---|
:hover | Mouse is over the element |
:focus | Element has keyboard focus |
:focus-visible | Focus from keyboard (not mouse click) |
:active | Element is being pressed/clicked |
:visited | Link has been visited |
.btn:hover { background: #1d4ed8; }
.btn:focus-visible { outline: 2px solid #3b82f6; outline-offset: 2px; }
.btn:active { transform: scale(0.98); }
Structural Pseudo-Classes
| Pseudo-class | What it selects |
|---|---|
:first-child | First child of its parent |
:last-child | Last child of its parent |
:nth-child(n) | n-th child (1-indexed) |
:nth-child(odd) | Odd children (1st, 3rd, 5th...) |
:nth-child(even) | Even children (2nd, 4th, 6th...) |
:nth-child(3n) | Every 3rd child |
:nth-child(3n+1) | 1st, 4th, 7th... (every 3rd starting from 1) |
:only-child | Element with no siblings |
:empty | Element with no children or text |
Form Pseudo-Classes
| Pseudo-class | Matches |
|---|---|
:checked | Checked checkboxes/radio buttons |
:disabled | Disabled form elements |
:required | Required fields |
:valid / :invalid | Valid/invalid form inputs |
:placeholder-shown | Input currently showing placeholder text |
5. Modern Selectors: :is(), :where(), :not(), :has()
These are game-changers for writing cleaner, more powerful CSS.
:is() — Shorter Selectors
is() lets you group selectors without repeating the common part:
/* ❌ Without :is() — verbose */
article h1,
article h2,
article h3 {
color: #1e293b;
}
/* ✅ With :is() — clean */
article :is(h1, h2, h3) {
color: #1e293b;
}
Key: :is() takes the highest specificity of its arguments.
:where() — Zero Specificity
:where() works exactly like :is(), but with zero specificity. This makes it perfect for default styles that should be easily overridden:
/* Default link styles (easy to override) */
:where(a) {
color: #3b82f6;
text-decoration: none;
}
/* This wins because regular selectors have higher specificity */
.custom-link {
color: red; /* Always wins over :where(a) */
}
:not() — Exclusion
not() selects elements that do NOT match a selector:
/* Style all buttons EXCEPT disabled ones */
.btn:not(:disabled) {
cursor: pointer;
}
/* All list items except the last one get a border */
li:not(:last-child) {
border-bottom: 1px solid #e5e7eb;
}
:has() — The Parent Selector
:has() is the holy grail CSS developers have wanted for decades. It selects an element based on what it contains:
/* Card that CONTAINS an image */
.card:has(img) {
padding: 0;
}
/* Form field with an invalid input inside */
.form-group:has(:invalid) {
border-color: #ef4444;
}
/* Container that has a checked checkbox */
.option:has(input:checked) {
background: #dbeafe;
border-color: #3b82f6;
}
6. Specificity: How Conflicts Are Resolved
When multiple rules target the same element, specificity determines which one wins.
The Scoring System
| Selector Type | Score | Example |
|---|---|---|
| Inline styles | 1,0,0,0 | style="color: red" |
| ID | 0,1,0,0 | #header |
| Class, attribute, pseudo-class | 0,0,1,0 | .card, [type], :hover |
| Type, pseudo-element | 0,0,0,1 | div, ::before |
| Universal (*), combinators | 0,0,0,0 | *, >, + |
Calculating Specificity
/* (0,0,0,1) — one type selector */
p { color: black; }
/* (0,0,1,0) — one class */
.text { color: blue; }
/* (0,0,1,1) — one class + one type */
p.text { color: green; }
/* (0,1,0,0) — one ID */
#intro { color: red; }
/* (0,1,1,1) — ID + class + type */
#intro p.text { color: purple; }
| Selector | Specificity | Winner? |
|---|---|---|
| p | (0,0,0,1) | Lowest |
| .text | (0,0,1,0) | ↑ |
| p.text | (0,0,1,1) | ↑ |
| #intro | (0,1,0,0) | ↑ |
| #intro .text | (0,1,1,0) | Highest ✓ |
7. The :nth-child() Deep Dive
Common Patterns
| Expression | Selects |
|---|---|
:nth-child(3) | The 3rd child |
:nth-child(odd) | 1st, 3rd, 5th... |
:nth-child(even) | 2nd, 4th, 6th... |
:nth-child(3n) | Every 3rd (3, 6, 9...) |
:nth-child(3n+1) | 1, 4, 7, 10... |
:nth-child(-n+3) | First 3 children only |
:nth-child(n+4) | 4th child and beyond |
:nth-last-child(2) | 2nd from the end |
The of S Syntax (Modern)
The newer of S syntax lets you filter by selector:
/* Select the 2nd item that has class .important */
:nth-child(2 of .important) {
background: yellow;
}
This is different from :nth-child(2).important, which selects the 2nd child only if it also has .important.
8. Pseudo-Elements: Styling Parts of Elements
Pseudo-elements style specific parts of an element. They use double colons (::) to distinguish them from pseudo-classes.
| Pseudo-element | What it targets |
|---|---|
::before | Inserts content before the element |
::after | Inserts content after the element |
::first-line | First line of text |
::first-letter | First letter of text |
::placeholder | Placeholder text in inputs |
::selection | Text selected by the user |
::marker | List item bullets/numbers |
/* Decorative line before headings */
h2::before {
content: "";
display: inline-block;
width: 4px;
height: 1em;
background: #3b82f6;
margin-right: 0.5rem;
border-radius: 2px;
vertical-align: middle;
}
Important:
::beforeand::afterrequire thecontentproperty to work. Even if you don't want text, usecontent: "".
9. Practical Selector Patterns
Pattern 1: Zebra Stripe Table
tbody tr:nth-child(odd) {
background: #f8fafc;
}
Pattern 2: Separator Between Items
li:not(:last-child) {
border-bottom: 1px solid #e5e7eb;
}
Pattern 3: Highlight Required Fields
input:required:invalid {
border-color: #ef4444;
}
input:required:valid {
border-color: #10b981;
}
Pattern 4: Style Based on Sibling Count
/* If an item is the only child */
.tag:only-child {
width: 100%;
}
/* If there are exactly 3 items */
.tag:first-child:nth-last-child(3),
.tag:first-child:nth-last-child(3) ~ .tag {
width: 33.33%;
}
10. Common Mistakes & Gotchas
Mistake 1: Over-Specific Selectors
/* ❌ Too specific — hard to override */
div.container section.main article.post p.text { color: #333; }
/* ✅ Just enough specificity */
.post-text { color: #333; }
Mistake 2: Confusing :nth-child with :nth-of-type
/* :nth-child(2) — second child regardless of type */
p:nth-child(2) { } /* Only matches if the 2nd child IS a <p> */
/* :nth-of-type(2) — second <p> among its siblings */
p:nth-of-type(2) { } /* Always matches the 2nd <p> */
Mistake 3: Using IDs for Styling
IDs have specificity of (0,1,0,0) — higher than any combination of classes. This makes them hard to override without !important. Use classes instead.
Mistake 4: Not Using :focus-visible
/* ❌ Annoying focus ring on mouse clicks */
button:focus { outline: 2px solid blue; }
/* ✅ Focus ring only for keyboard navigation */
button:focus-visible { outline: 2px solid blue; }
Conclusion
CSS selectors are your precision tools for targeting elements:
- Basic: Type, class, ID, attribute selectors
- Combinators: Descendant (space), child (>), sibling (+, ~)
- Pseudo-classes: State (:hover, :focus), structural (:nth-child, :first-child)
- Modern:
:is()for grouping,:where()for zero-specificity defaults,:not()for exclusion,:has()for parent selection - Pseudo-elements:
::before,::after,::selection,::marker
The key to mastering selectors is understanding specificity and choosing the right level of specificity for each situation. Keep things simple, use classes as your primary targeting mechanism, and reach for IDs and complex selectors only when truly needed.
🧠 Test Your Knowledge
Now that you've learned the concepts, let's see if you can apply them! Take this quick quiz to test your understanding.