CSS Selectors — Complete Guide with Examples
In this tutorial, you'll learn about CSS Selectors. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
CSS selectors are patterns that tell the browser which HTML elements to style. Beyond the basic type, class, and id selectors, CSS provides powerful tools like combinators, attribute selectors, pseudo-classes, and pseudo-elements that let you target elements with surgical precision.
What You'll Learn
By the end of this tutorial, you'll understand combinators (descendant, child, sibling) for context-based targeting, attribute selectors for targeting by attribute values, pseudo-classes for state and position-based styling, pseudo-elements for styling parts of elements, and modern selectors like :has(), :is(), and :not().
Prerequisite: You should understand basic CSS selectors (type, class, id). CSS Basics covers the fundamentals.
Where This Fits
flowchart LR
A["CSS Basics"] --> B["**CSS Selectors**"]
B --> C["CSS Cascade & Specificity"]
C --> D["Box Model"]
D --> E["Layout (Flexbox & Grid)"]
style B fill:#f97316,stroke:#c2410c,color:#fff
style A fill:#e5e7eb,stroke:#9ca3af,color:#374151
style E fill:#22c55e,stroke:#16a34a,color:#fff
Combinators — Target Elements by Their Position
Combinators let you style elements based on where they appear in the HTML structure — inside other elements, as direct children, or next to siblings.
| Combinator | Symbol | Example | What it targets |
|---|---|---|---|
| Descendant | (space) | div p |
Any <p> anywhere inside a <div> |
| Child | > |
div > p |
<p> that is a direct child of a <div> |
| Adjacent sibling | + |
h2 + p |
The first <p> immediately after an <h2> |
| General sibling | ~ |
h2 ~ p |
All <p> elements that are siblings after an <h2> |
Descendant vs Child — The Most Important Difference
<div class="card">
<h2>Title</h2>
<p>Direct child paragraph</p>
<div class="footer">
<p>Nested paragraph (not a direct child)</p>
</div>
</div>
/* Descendant: targets BOTH paragraphs (any <p> inside .card) */
.card p { color: blue; }
/* Child: targets ONLY the direct child paragraph */
.card > p { color: blue; }
Why this matters: The descendant selector (space) is broad — it catches everything nested at any depth. The child selector (>) is strict — only immediate children. Use child selector when you want to avoid styling deeply nested elements.
Adjacent and General Siblings
/* Adjacent: only the paragraph IMMEDIATELY after h2 */
h2 + p { font-weight: bold; }
/* General: ALL paragraphs after h2 */
h2 ~ p { margin-left: 1rem; }
Real-world use: Style the first paragraph after a heading differently (make it a lead paragraph), or indent all content after a heading.
Attribute Selectors — Target by HTML Attributes
Attribute selectors target elements based on their attributes and attribute values.
/* Has the attribute (regardless of value) */
[disabled] { opacity: 0.5; cursor: not-allowed; }
/* Exact attribute value match */
[type="text"] { border-color: #3498db; }
[type="submit"] { background: #27ae60; color: white; }
/* Starts with (useful for links) */
a[href^="https"] { color: #27ae60; }
a[href^="mailto"]::before { content: "✉ "; }
/* Ends with (useful for file types) */
img[src$=".png"] { border: 2px solid #3498db; }
a[href$=".pdf"]::after { content: " (PDF)"; }
/* Contains substring */
a[href*="example.com"] { font-weight: bold; }
Real-world use: Style external links differently with a[href^="https"], add file type icons with a[href$=".pdf"], highlight required form fields with [required].
Pseudo-Classes — Style by State or Position
Pseudo-classes start with a single colon (:) and style elements based on their state or position in the document.
Interactive States (User Action)
/* Link states (order matters: LVHA) */
a:link { color: #3498db; } /* Unvisited link */
a:visited { color: #8e44ad; } /* Visited link */
a:hover { text-decoration: underline; } /* Hover */
a:active { color: #e74c3c; } /* While clicking */
/* Form states */
input:focus { outline: 2px solid #3498db; } /* Focused input */
input:disabled { background: #eee; } /* Disabled */
input:required { border-left: 3px solid #e74c3c; }
/* Validation states */
input:valid { border-color: #27ae60; }
input:invalid { border-color: #e74c3c; }
Structural Pseudo-Classes (Position)
/* First and last */
li:first-child { font-weight: bold; }
li:last-child { border: none; }
/* First/last of their type */
p:first-of-type { margin-top: 0; }
p:last-of-type { margin-bottom: 0; }
/* nth-child patterns */
li:nth-child(odd) { background: #f8f9fa; } /* Every odd */
li:nth-child(even) { background: white; } /* Every even */
li:nth-child(3) { color: #e74c3c; } /* Third child only */
li:nth-child(3n) { color: #e74c3c; } /* Every third */
li:nth-child(3n+1) { color: blue; } /* First in each group of 3 */
/* Empty elements */
p:empty { display: none; }
The n in nth-child: Think of it as a counter starting from 0. 3n matches 3, 6, 9, 12... 3n+1 matches 1, 4, 7, 10...
Modern Pseudo-Classes
/* :not() — everything EXCEPT what matches */
input:not([type="submit"]) { border: 1px solid #ddd; }
li:not(.active) { opacity: 0.7; }
/* :has() — parent selector (widely supported since 2024) */
/* Style a card that CONTAINS an image */
.card:has(img) { padding: 0; }
/* Style a form that has an invalid input */
form:has(:invalid) { border-color: #e74c3c; }
/* :is() — group selectors with forgiving specificity */
:is(h1, h2, h3) { font-family: 'Georgia', serif; }
:is(h1, h2, h3) .title { color: #2c3e50; }
/* :where() — same as :is() but zero specificity */
:where(header, footer) p { color: #666; }
/* :target — element whose ID matches the URL hash */
:target { background: #ffeaa7; padding: 8px; border-radius: 4px; }
Why :has() is exciting: For years, CSS couldn't style a parent based on its children. :has() changes that. If you want to add a border to a card that contains an image — you can do that now without JavaScript.
Pseudo-Elements — Style Parts of Elements
Pseudo-elements use double colons (::) and let you style parts of an element or insert content before/after it.
/* Insert content before or after an element */
.note::before { content: "💡 "; }
.quote::after { content: " —Source"; }
/* Style the first letter or first line */
p::first-letter { font-size: 2em; float: left; color: #e74c3c; }
p::first-line { font-weight: bold; }
/* Selection highlight */
::selection { background: #3498db; color: white; }
/* Input placeholder text */
input::placeholder { color: #999; font-style: italic; }
/* List marker (bullet/number) */
li::marker { color: #e74c3c; }
Tooltip with Pseudo-Elements
[data-tooltip] {
position: relative;
}
[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: #333;
color: white;
padding: 4px 8px;
border-radius: 4px;
white-space: nowrap;
font-size: 0.875rem;
opacity: 0;
transition: opacity 0.2s;
pointer-events: none;
}
[data-tooltip]:hover::after {
opacity: 1;
}
<button data-tooltip="Save your changes">Save</button>
How it works: The tooltip text is stored in a data-tooltip attribute. content: attr(data-tooltip) reads that attribute and displays it. On hover, the tooltip fades in.
Common Selector Mistakes
1. Confusing Descendant and Child
/* ❌ Child selector doesn't match deeply nested elements */
.card > p { /* misses <p> inside <div class="card"><div><p>...</p></div></div> */ }
/* ✅ Descendant matches at any depth */
.card p { /* matches all */ }
2. Forgetting the nth-child Formula
/* ❌ nth-child(2n) starts at 0, so 0, 2, 4 — that's even */
li:nth-child(2n) { background: red; } /* This is EVEN, not every 2nd */
/* ✅ Use the right formula */
li:nth-child(even) { background: #f8f9fa; } /* More readable */
li:nth-child(2n) { background: #f8f9fa; } /* Same thing */
3. Wrong Pseudo-Class Syntax
/* ❌ Double colon for pseudo-class (wrong) */
p::hover { color: red; }
/* ✅ Single colon for pseudo-class */
p:hover { color: red; }
/* ❌ Single colon for pseudo-element (old syntax, works but deprecated) */
p:before { content: "→ "; }
/* ✅ Double colon for pseudo-element */
p::before { content: "→ "; }
4. Overusing !important Instead of Specificity
When a selector isn't working, beginners add !important. Instead, use a more specific selector:
/* ❌ Wrong: !important is a band-aid */
.card { color: blue !important; }
/* ✅ Correct: use specificity */
body .card { color: blue; }
5. Not Considering Accessibility with :hover-Only Effects
/* ❌ Tooltip only shows on hover — mobile users never see it */
[data-tooltip]::after { opacity: 0; }
[data-tooltip]:hover::after { opacity: 1; }
/* ✅ Also show on focus for keyboard users */
[data-tooltip]:focus::after { opacity: 1; }
Try It Yourself
See selectors in action:
Common Mistakes Beginners Make
1. Skipping the Fundamentals
Many beginners jump straight to advanced topics without mastering the basics. Take time to understand the core concepts before moving on.
2. Not Practicing Enough
Reading tutorials without writing code leads to shallow understanding. Code along with every example and experiment on your own.
3. Ignoring Error Messages
Error messages tell you exactly what went wrong. Read them carefully — they usually point to the line and type of issue.
4. Copy-Pasting Without Understanding
It's tempting to copy code from tutorials, but typing it yourself and understanding each line builds real skill.
5. Giving Up Too Early
Every developer hits frustrating bugs. Take breaks, ask for help, and remember that struggling is part of learning.
Practice Questions
Q1: What's the difference between div p and div > p?
{{< details "Show answer" >}}
div p (descendant) targets any <p> anywhere inside <div>. div > p (child) targets only <p> that are direct children of <div>.
{{< /details >}}
Q2: What does a[href^="https"] select?
{{< details "Show answer" >}}
All <a> elements whose href attribute starts with "https" — useful for styling external links differently from internal ones.
{{< /details >}}
Q3: What's the difference between :first-child and :first-of-type?
{{< details "Show answer" >}}
:first-child targets the first child of its parent regardless of type. :first-of-type targets the first element of its specific type (first <p>, first <h2>, etc.).
{{< /details >}}
Q4: What does ::before do and what property controls its content?
{{< details "Show answer" >}}
It inserts content before an element. The content property (e.g., content: "→ ") controls what's inserted.
{{< /details >}}
Q5: What does :has() allow you to do that wasn't previously possible in CSS?
{{< details "Show answer" >}}
It chooses a parent element based on its children (e.g., .card:has(img) selects cards that contain an image). This was only possible with JavaScript before.
{{< /details >}}
Challenge
Build a styled "Task List" without using any classes or IDs — only type selectors, combinators, and pseudo-classes:
- Alternating row colors
- Checked items have strikethrough
- Hover highlights the row
- The first and last items have special styles
- Empty items are hidden
FAQ
FAQ
What's Next?
Now learn how CSS resolves conflicts between selectors:
What's Next
Congratulations on completing this Css Selectors tutorial! Here's where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro