Chapter 12: CSS Pseudo-Classes & Pseudo-Elements
Pseudo-classes target elements based on state, DOM structure, or user interaction, while pseudo-elements let you style specific parts of an element or inject virtual sub-elements without adding HTML tags. Mastering structural selectors, state triggers, relational logic (:has(), :is(), :not()), and pseudo-elements unlocks next-level CSS capabilities.
12.1 Structural Pseudo-Classes (:nth-child, :first-of-type, :last-child)
Structural pseudo-classes select elements based on their position within the parent DOM tree order.
| Selector | Syntax Pattern | Matching Rule |
|---|---|---|
:nth-child(n) |
:nth-child(2n), :nth-child(odd), :nth-child(3n+1) |
Matches the n-th child of its parent, regardless of element type. |
:first-child / :last-child |
li:first-child, li:last-child |
Matches the first or last child element inside a parent container. |
:first-of-type / :last-of-type |
p:first-of-type |
Matches the first/last occurrence of a specific tag type among siblings. |
:only-child |
span:only-child |
Matches an element only if it is the sole child inside its parent. |
<!DOCTYPE html>
<html>
<head>
<style>
*, *::before, *::after { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; background: #0f172a; color: #f8fafc; padding: 24px; margin: 0; }
h3 { margin-top: 0; color: #818cf8; }
.list-demo { list-style: none; padding: 0; margin: 0 0 20px; }
.list-demo li {
background: #1e293b; border: 1px solid #334155;
padding: 10px 16px; margin-bottom: 6px; border-radius: 6px;
font-size: 13px; color: #94a3b8;
}
/* :nth-child(even) zebra striping */
.zebra li:nth-child(even) { background: rgba(99, 102, 241, 0.2); border-color: #6366f1; color: #a5b4fc; }
/* :first-child & :last-child */
.range li:first-child { border-left: 4px solid #10b981; color: #34d399; font-weight: 700; }
.range li:last-child { border-left: 4px solid #ef4444; color: #f87171; font-weight: 700; }
</style>
</head>
<body>
<h3>Structural Pseudo-Classes Demo</h3>
<p style="font-size:12px;color:#64748b;">:nth-child(even) — Alternating row highlights:</p>
<ul class="list-demo zebra">
<li>Item 1 (odd)</li>
<li>Item 2 (even — highlighted)</li>
<li>Item 3 (odd)</li>
<li>Item 4 (even — highlighted)</li>
</ul>
<p style="font-size:12px;color:#64748b;">:first-child (green bar) & :last-child (red bar):</p>
<ul class="list-demo range">
<li>First Child Item</li>
<li>Middle Child Item</li>
<li>Last Child Item</li>
</ul>
</body>
</html>
12.2 Form & State Pseudo-Classes (:focus-visible, :checked, :disabled)
State pseudo-classes react dynamically to user input actions, form field validation states, and accessibility focus management.
<!DOCTYPE html>
<html>
<head>
<style>
*, *::before, *::after { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; background: #0f172a; color: #f8fafc; padding: 24px; margin: 0; }
h3 { margin-top: 0; color: #38bdf8; }
.form-group { margin-bottom: 16px; max-width: 360px; }
label { display: block; font-size: 12px; color: #94a3b8; margin-bottom: 6px; }
input[type="text"] {
width: 100%; padding: 10px 14px; background: #1e293b;
border: 1px solid #334155; border-radius: 8px; color: #fff; font-size: 14px;
}
/* :focus-visible — keyboard focus ring without mouse click outlines */
input[type="text"]:focus-visible {
outline: 2px solid #38bdf8;
border-color: #38bdf8;
box-shadow: 0 0 10px rgba(56,189,248,0.3);
}
/* :disabled state styling */
input:disabled {
background: #0f172a; border-color: #1e293b; opacity: 0.5; cursor: not-allowed;
}
/* Custom Checkbox using :checked + label */
.checkbox-label { display: flex; align-items: center; gap: 10px; cursor: pointer; user-select: none; }
.checkbox-label input[type="checkbox"] { display: none; }
.custom-box {
width: 20px; height: 20px; border: 2px solid #6366f1; border-radius: 4px;
display: flex; align-items: center; justify-content: center; transition: all 0.2s;
}
/* When input is :checked, style adjacent .custom-box */
input[type="checkbox"]:checked + .custom-box {
background: #6366f1; border-color: #6366f1;
}
input[type="checkbox"]:checked + .custom-box::after {
content: "✔"; color: #fff; font-size: 12px; font-weight: bold;
}
</style>
</head>
<body>
<h3>Form State Pseudo-Classes</h3>
<div class="form-group">
<label>Focus field (:focus-visible ring):</label>
<input type="text" placeholder="Click or Tab here..." />
</div>
<div class="form-group">
<label>Disabled Input (:disabled):</label>
<input type="text" value="Disabled input field" disabled />
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" checked />
<div class="custom-box"></div>
<span style="font-size:14px;color:#f8fafc;">Custom Checkbox (:checked state)</span>
</label>
</div>
</body>
</html>
12.3 Relational & Logic Pseudo-Classes (:not, :is, :where, :has())
CSS3 & CSS4 introduced powerful logic functions that reduce selector duplication and enable parent styling based on child elements.
| Logic Selector | Syntax & Example | Key Benefit |
|---|---|---|
:not(selector) |
button:not(.primary) |
Selects all elements EXCEPT those matching the argument. |
:is(s1, s2) |
:is(h1, h2, h3) span |
Groups multiple selectors into a single line; takes specificity of highest argument. |
:where(s1, s2) |
:where(h1, h2, h3) span |
Identical to :is() but has 0 specificity (easy to override in design systems). |
:has(selector) |
.card:has(img) |
The Parent Selector! Styles a parent element if it contains a matching descendant. |
<!DOCTYPE html>
<html>
<head>
<style>
*, *::before, *::after { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; background: #0f172a; color: #f8fafc; padding: 24px; margin: 0; }
h3 { margin-top: 0; color: #f59e0b; }
.card {
background: #1e293b; border: 2px solid #334155; border-radius: 10px;
padding: 16px; margin-bottom: 12px; max-width: 400px; transition: all 0.3s;
}
/* :has() — Style parent card if it contains a featured badge */
.card:has(.badge-featured) {
border-color: #f59e0b;
background: rgba(245, 158, 11, 0.1);
box-shadow: 0 4px 16px rgba(245, 158, 11, 0.2);
}
.badge-featured {
display: inline-block; background: #f59e0b; color: #0f172a;
padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 800;
}
/* :not() — Style all buttons EXCEPT .btn-danger */
.btn { background: #3b82f6; color: #fff; border: none; padding: 8px 14px; border-radius: 6px; margin-right: 6px; cursor: pointer; }
.btn:not(.btn-danger):hover { background: #60a5fa; }
.btn-danger { background: #ef4444; }
</style>
</head>
<body>
<h3>Logic Pseudo-Classes — :has() & :not()</h3>
<p style="font-size:12px;color:#64748b;">:has() — Card 2 is highlighted because it contains .badge-featured:</p>
<div class="card">Standard Card (no featured badge)</div>
<div class="card">
<span class="badge-featured">FEATURED</span>
<p style="margin:6px 0 0;font-size:13px;color:#cbd5e1;">This parent card automatically turned amber because of <code>.card:has(.badge-featured)</code>!</p>
</div>
<p style="font-size:12px;color:#64748b;margin-top:16px;">:not() — Hover both buttons (only non-danger gets light blue hover):</p>
<button class="btn">Normal Button</button>
<button class="btn btn-danger">Danger Button (:not excluded)</button>
</body>
</html>
12.4 Pseudo-Elements (::before, ::after, ::placeholder, ::selection)
Pseudo-elements style specific parts of an element or create virtual sub-elements without altering your HTML markup.
| Pseudo-Element | Target Effect | Common Use Case |
|---|---|---|
::before |
Injects virtual element before content | Icons, decorative quote marks, custom bullets. |
::after |
Injects virtual element after content | Clearfix clears, tooltips, animated underline bars. |
::placeholder |
Styles input placeholder text | Customizing placeholder font color and opacity. |
::selection |
Styles text highlighted by mouse drag | Customizing highlight selection background & text color. |
::first-letter |
Styles the very first letter | Creating editorial drop caps in articles. |
<!DOCTYPE html>
<html>
<head>
<style>
*, *::before, *::after { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; background: #0f172a; color: #f8fafc; padding: 24px; margin: 0; }
h3 { margin-top: 0; color: #10b981; }
/* ::selection — Custom text highlight color */
::selection {
background: #6366f1;
color: #ffffff;
}
/* ::first-letter — Drop cap editorial style */
.article-intro::first-letter {
font-size: 3rem;
float: left;
line-height: 0.8;
margin-right: 8px;
color: #10b981;
font-weight: 900;
}
/* ::placeholder styling */
input::placeholder {
color: #64748b;
font-style: italic;
}
input {
padding: 10px 14px; background: #1e293b; border: 1px solid #334155;
border-radius: 6px; color: #fff; width: 100%; max-width: 320px;
}
/* ::after animated underline link */
.fancy-link {
color: #38bdf8; text-decoration: none; font-weight: 700;
position: relative; display: inline-block; padding-bottom: 4px;
}
.fancy-link::after {
content: "";
position: absolute; bottom: 0; left: 0;
width: 0; height: 2px;
background: #38bdf8;
transition: width 0.3s ease;
}
.fancy-link:hover::after {
width: 100%;
}
</style>
</head>
<body>
<h3>Pseudo-Elements Demo</h3>
<p style="font-size:12px;color:#64748b;">Highlight text below with your mouse (custom ::selection color!):</p>
<p class="article-intro" style="font-size:14px;color:#cbd5e1;line-height:1.6;">
Cascading Style Sheets allow developers to inject virtual pseudo-elements without polluting HTML DOM markup with extra spans or divs.
</p>
<p style="font-size:12px;color:#64748b;margin-top:16px;">::placeholder styling:</p>
<input type="text" placeholder="Custom italic placeholder..." />
<p style="font-size:12px;color:#64748b;margin-top:16px;">::after animated underline on hover:</p>
<a href="#" class="fancy-link">Hover over this link for underline animation →</a>
</body>
</html>
12.5 Component Project: Interactive Task Card with :has() State Management
This component uses the relational parent pseudo-class :has() to dynamically restyle the entire parent card when a child checkbox is checked — with zero JavaScript:
<!DOCTYPE html>
<html>
<head>
<style>
*, *::before, *::after { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; background: #0f172a; color: #f8fafc; padding: 24px; margin: 0; }
.task-card {
background: #1e293b; border: 2px solid #334155; border-radius: 12px;
padding: 20px; max-width: 400px; transition: all 0.3s ease;
}
/* Parent styling based on child state */
.task-card:has(input:checked) {
border-color: #10b981; background: rgba(16, 185, 129, 0.15);
}
.task-card:has(input:checked) .task-title {
text-decoration: line-through; color: #64748b;
}
.task-card:has(input:checked) .badge {
background: #10b981; color: #fff;
}
.task-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.task-title { font-weight: 700; font-size: 15px; margin: 0; color: #f8fafc; transition: all 0.3s; }
.badge { background: #334155; color: #94a3b8; font-size: 11px; font-weight: 800; padding: 4px 10px; border-radius: 999px; transition: all 0.3s; }
.checkbox-label { display: flex; align-items: center; gap: 10px; cursor: pointer; font-size: 13px; color: #cbd5e1; }
.checkbox-label input { width: 18px; height: 18px; accent-color: #10b981; cursor: pointer; }
</style>
</head>
<body>
<div class="task-card">
<div class="task-header">
<h4 class="task-title">Deploy Production Release</h4>
<span class="badge">PENDING</span>
</div>
<label class="checkbox-label">
<input type="checkbox" /> Mark task as complete (styler via :has())
</label>
</div>
</body>
</html>
12.6 Interactive Sandbox — Pseudo-Classes & Pseudo-Elements
Experiment live with :nth-child, :has() parent selection, ::selection highlights, and ::after hover bars in the playground below: