CSS Pseudo-classes & Pseudo-elements — The Complete Deep-Dive Guide
Master every CSS pseudo-class and pseudo-element — user states, structural selectors, :is() :has() :where(), ::before ::after, and modern pseudo-selectors used in professional UI development.
Introduction
Pseudo-classes and pseudo-elements are CSS's way of styling things that don't exist as separate elements in your HTML. With them, you can style an element when the user hovers it, target only the third list item, create decorative graphics without any HTML, style placeholder text, and even select a parent based on its children.
The difference between the two:
- Pseudo-class (
:) — Targets an element in a specific state or position.a:hover,li:first-child - Pseudo-element (
::) — Creates a virtual element that you can style.p::first-line,div::before
1. User Action Pseudo-classes
These respond to how the user interacts with elements.
:hover
Triggers when the mouse cursor is over the element.
.btn {
background: #7c3aed;
transition: background 0.2s ease, transform 0.2s ease;
}
.btn:hover {
background: #6d28d9;
transform: translateY(-2px);
}
/* Hover on parent affects child */
.card:hover .card-title {
color: var(--color-primary);
}
:focus
Triggers when the element receives keyboard or programmatic focus. Critical for accessibility.
input:focus {
outline: 2px solid #7c3aed;
outline-offset: 2px;
border-color: #7c3aed;
}
/* Never do this without an alternative */
/* *:focus { outline: none; } ← Kills keyboard navigation */
:focus-visible
Like :focus, but only applies when focus is visible — i.e., via keyboard, not mouse click. Lets you show focus rings for keyboard users without showing them on mouse clicks.
/* Remove outline on mouse click, keep for keyboard */
.btn:focus {
outline: none;
}
.btn:focus-visible {
outline: 2px solid #7c3aed;
outline-offset: 3px;
}
:focus-within
Matches an element if it or any descendant has focus. Great for styling form groups.
.form-group {
border: 1.5px solid #e5e7eb;
border-radius: 8px;
padding: 12px 16px;
transition: border-color 0.2s ease;
}
.form-group:focus-within {
border-color: #7c3aed;
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.15);
}
:active
Triggers while the element is being activated (mouse down, touch start).
.btn:active {
transform: scale(0.97);
box-shadow: none;
}
:visited
Matches links the user has already visited.
a:visited {
color: #7c3aed;
}
2. Structural Pseudo-classes
These target elements based on their position in the document tree.
:first-child and :last-child
/* Remove top border from first list item */
.list-item:first-child {
border-top: none;
}
/* Remove bottom border from last */
.list-item:last-child {
border-bottom: none;
margin-bottom: 0;
}
:nth-child()
One of the most powerful selectors. Accepts a number, keyword, or formula.
/* Specific position */
li:nth-child(3) { font-weight: bold; } /* 3rd item */
/* Keywords */
li:nth-child(odd) { background: #f9fafb; } /* 1st, 3rd, 5th... */
li:nth-child(even) { background: #ffffff; } /* 2nd, 4th, 6th... */
/* Formula: An+B */
/* n starts at 0 and counts up: 0, 1, 2, 3... */
li:nth-child(3n) { color: red; } /* 3rd, 6th, 9th... (every 3rd) */
li:nth-child(3n+1) { color: blue; } /* 1st, 4th, 7th... */
li:nth-child(3n+2) { color: green; } /* 2nd, 5th, 8th... */
li:nth-child(n+4) { opacity: 0.5; } /* 4th item onwards */
li:nth-child(-n+3) { font-weight: bold; } /* First 3 items only */
:nth-of-type() vs :nth-child()
:nth-child() counts ALL siblings. :nth-of-type() counts only siblings of the same element type.
/* Given: h2, p, p, h2, p */
/* Selects the 2nd child — which is first <p> */
p:nth-child(2) { color: red; }
/* Selects the 2nd <p> — regardless of position */
p:nth-of-type(2) { color: blue; }
:only-child and :only-of-type
/* Style differently when there's only one item */
.card:only-child {
margin: 0 auto;
max-width: 600px;
}
/* Only <p> of its type in the parent */
p:only-of-type {
font-style: italic;
}
:empty
Matches elements with no children (including text nodes).
/* Hide empty elements */
.notification-badge:empty {
display: none;
}
/* Style empty table cells */
td:empty::before {
content: "—";
color: #9ca3af;
}
:root
Matches the document root element (<html>). Identical to html but higher specificity. Use for CSS custom properties.
:root {
--color-primary: #7c3aed;
--font-size-base: 1rem;
}
3. Form Pseudo-classes
Input State Classes
/* Required fields */
input:required {
border-left: 3px solid #e53935;
}
input:optional {
border-left: 3px solid #e5e7eb;
}
/* Validation states */
input:valid {
border-color: #22c55e;
}
input:invalid {
border-color: #ef4444;
}
/* Only show invalid style after user has interacted */
input:not(:placeholder-shown):invalid {
border-color: #ef4444;
background: #fef2f2;
}
:disabled and :enabled
input:disabled,
button:disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
input:enabled {
cursor: text;
}
:checked
Matches checkboxes and radio buttons that are checked.
/* Custom checkbox */
.checkbox-input {
display: none;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.checkbox-label::before {
content: '';
width: 18px;
height: 18px;
border: 2px solid #d1d5db;
border-radius: 4px;
transition: all 0.2s ease;
flex-shrink: 0;
}
.checkbox-input:checked + .checkbox-label::before {
background: #7c3aed;
border-color: #7c3aed;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3E%3C/svg%3E");
}
:placeholder-shown
Matches an input when its placeholder is currently visible (i.e., the field is empty).
/* Floating label technique */
.field {
position: relative;
}
.field input {
padding: 20px 16px 8px;
}
.field label {
position: absolute;
top: 14px;
left: 16px;
font-size: 1rem;
color: #9ca3af;
transition: all 0.2s ease;
pointer-events: none;
}
/* When placeholder is visible (field is empty) — label is big */
.field input:placeholder-shown + label {
top: 14px;
font-size: 1rem;
}
/* When field has content — label floats up */
.field input:not(:placeholder-shown) + label,
.field input:focus + label {
top: 4px;
font-size: 0.72rem;
color: #7c3aed;
}
:read-only and :read-write
input:read-only {
background: #f9fafb;
color: #6b7280;
cursor: default;
}
input:read-write:focus {
border-color: #7c3aed;
}
4. Logical Pseudo-classes
:not() — The Negation Selector
Selects elements that do NOT match the argument. Modern :not() accepts complex selectors.
/* All buttons except .primary */
.btn:not(.primary) {
background: transparent;
border: 1.5px solid currentColor;
}
/* All links not in the nav */
a:not(.nav a) {
text-decoration: underline;
}
/* All form inputs except checkboxes and radios */
input:not([type="checkbox"]):not([type="radio"]) {
border: 1.5px solid #e5e7eb;
border-radius: 6px;
padding: 10px 14px;
}
/* All list items except the last */
li:not(:last-child) {
border-bottom: 1px solid #e5e7eb;
}
:is() — Matches Any
Groups multiple selectors. Takes the specificity of its most specific argument. Much cleaner than repeating selectors.
/* Without :is() — verbose */
h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
text-decoration: none;
}
/* With :is() — clean */
:is(h1, h2, h3, h4, h5, h6) a {
text-decoration: none;
}
/* Real-world: consistent typography in content areas */
:is(article, .prose, .content) :is(p, ul, ol, blockquote) {
margin-bottom: 1.5rem;
line-height: 1.75;
}
:where() — Zero Specificity Grouping
Identical to :is() but with zero specificity. Perfect for base styles you want to be easily overridable.
/* These styles have 0 specificity — easy to override */
:where(h1, h2, h3, h4) {
font-weight: 700;
line-height: 1.2;
margin-top: 0;
}
/* This single-class selector easily overrides :where() */
.display-heading {
font-weight: 800;
line-height: 1.1;
}
:has() — The Parent Selector
The most anticipated CSS feature in years. Selects an element based on whether it contains a matching descendant.
/* Card that has an image — remove top padding */
.card:has(img) {
padding-top: 0;
}
/* Form with invalid input — disable submit */
form:has(input:invalid) button[type="submit"] {
opacity: 0.5;
pointer-events: none;
}
/* Navigation item that has a dropdown — add arrow */
.nav-item:has(.dropdown)::after {
content: " ▾";
font-size: 0.75em;
}
/* Section that doesn't have a heading — warn in dev */
section:not(:has(h2, h3)) {
outline: 2px dashed orange;
}
/* Grid that has exactly 3 items */
.auto-grid:has(.item:nth-child(3):last-child) {
grid-template-columns: repeat(3, 1fr);
}
/* Figure with caption — add bottom padding */
figure:has(figcaption) {
padding-bottom: 8px;
}
:any-link
Matches all links (<a> with href, <area>, <link>). Equivalent to :link, :visited.
:any-link {
color: var(--color-link);
text-decoration: none;
}
5. ::before and ::after
The most versatile pseudo-elements. They create virtual child elements — first and last — inside the selected element. They must have a content property to appear.
.element::before {
content: ""; /* Empty — for decorative shapes */
content: "→"; /* Text */
content: attr(data-label); /* Dynamic from HTML attribute */
content: url('/icon.svg'); /* Image */
}
Decorative Line Under Headings
.section-title {
position: relative;
display: inline-block;
}
.section-title::after {
content: '';
position: absolute;
bottom: -8px;
left: 0;
width: 48px;
height: 3px;
background: var(--color-primary);
border-radius: 2px;
}
Quotation Marks
blockquote {
position: relative;
padding: 24px 32px;
font-style: italic;
}
blockquote::before {
content: '\201C'; /* Left double quotation mark */
position: absolute;
top: -8px;
left: 8px;
font-size: 6rem;
color: var(--color-primary);
opacity: 0.2;
line-height: 1;
font-family: Georgia, serif;
}
Tooltip via data-* Attribute
[data-tooltip] {
position: relative;
cursor: help;
}
[data-tooltip]::before {
content: attr(data-tooltip);
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
background: #1a1a1a;
color: white;
font-size: 0.75rem;
font-family: var(--font-sans);
padding: 6px 10px;
border-radius: 6px;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
}
[data-tooltip]::after {
content: '';
position: absolute;
bottom: calc(100% + 2px);
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: #1a1a1a;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
}
[data-tooltip]:hover::before,
[data-tooltip]:hover::after {
opacity: 1;
}
Usage:
<span data-tooltip="This is a tooltip">Hover me</span>
Clearfix (Legacy but Good to Know)
.clearfix::after {
content: '';
display: table;
clear: both;
}
Gradient Overlay on Images
.hero {
position: relative;
}
.hero::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(
to top,
rgba(0, 0, 0, 0.7) 0%,
transparent 50%
);
}
6. ::first-line and ::first-letter
::first-line
Styles only the first rendered line of a block element. Adjusts automatically as the viewport width changes.
article p::first-line {
font-variant: small-caps;
letter-spacing: 0.05em;
}
::first-letter
Styles only the very first letter. Classic drop cap effect.
.article-body > p:first-of-type::first-letter {
font-size: 4rem;
font-family: var(--font-display);
font-weight: 700;
float: left;
line-height: 0.8;
margin-right: 8px;
margin-top: 4px;
color: var(--color-primary);
}
7. ::placeholder
Styles the placeholder text of input elements.
input::placeholder {
color: #9ca3af;
font-style: italic;
font-size: 0.9em;
}
input:focus::placeholder {
opacity: 0.5;
transform: translateX(4px);
transition: all 0.2s ease;
}
8. ::selection
Styles the text the user has selected/highlighted.
::selection {
background: #7c3aed;
color: white;
}
/* Different selection per section */
.code-section::selection {
background: #1e3a5f;
color: #7dd3fc;
}
9. ::marker
Styles the bullet or number of list items.
/* Simple color */
li::marker {
color: var(--color-primary);
font-size: 1.2em;
}
/* Custom character */
ul li::marker {
content: "→ ";
color: #7c3aed;
}
/* Numbered list formatting */
ol li::marker {
font-weight: 700;
color: var(--color-primary);
font-family: var(--font-mono);
}
10. ::backdrop
Styles the backdrop behind a <dialog> element or fullscreen element.
dialog::backdrop {
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
}
11. Scrollbar Pseudo-elements (WebKit)
/* WebKit browsers (Chrome, Safari, Edge) */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #a1a1a1;
}
/* Firefox */
* {
scrollbar-width: thin;
scrollbar-color: #c1c1c1 #f1f1f1;
}
12. ::before Counter Trick — Automatic Numbering
.steps {
counter-reset: step-counter;
}
.step {
counter-increment: step-counter;
position: relative;
padding-left: 56px;
margin-bottom: 32px;
}
.step::before {
content: counter(step-counter);
position: absolute;
left: 0;
top: 0;
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--color-primary);
color: white;
font-weight: 700;
font-family: var(--font-sans);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.875rem;
}
13. Real-World Combined Patterns
Custom Focus Ring System
/* Base — remove browser default */
*:focus {
outline: none;
}
/* Show only on keyboard navigation */
*:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 3px;
border-radius: 3px;
}
/* Special handling for inputs */
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.25);
border-color: var(--color-primary);
}
Smart Link Styling
/* External links get an icon */
a[href^="http"]:not([href*="goswamidigitalworld.com"])::after {
content: " ↗";
font-size: 0.75em;
opacity: 0.6;
}
/* PDF links */
a[href$=".pdf"]::before {
content: "📄 ";
}
/* Email links */
a[href^="mailto"]::before {
content: "✉️ ";
}
Accessible Required Field Indicator
label:has(+ input:required)::after,
label:has(+ textarea:required)::after {
content: " *";
color: var(--color-error);
font-weight: 700;
}
Cheat Sheet
/* ── Pseudo-classes ── */
:hover /* Mouse over */
:focus /* Keyboard/programmatic focus */
:focus-visible /* Focus visible to user */
:focus-within /* Element or child has focus */
:active /* Being activated */
:visited /* Visited link */
:first-child /* First sibling */
:last-child /* Last sibling */
:nth-child(n) /* Nth sibling */
:nth-of-type(n) /* Nth of same tag */
:only-child /* Sole child */
:empty /* No children */
:root /* Document root */
:not(selector) /* Does not match */
:is(a, b, c) /* Matches any — inherits specificity */
:where(a, b) /* Matches any — zero specificity */
:has(selector) /* Contains matching descendant */
:required :optional
:valid :invalid
:checked :disabled :enabled
:placeholder-shown
:read-only :read-write
/* ── Pseudo-elements ── */
::before /* Virtual first child */
::after /* Virtual last child */
::first-line /* First rendered line */
::first-letter /* First letter */
::placeholder /* Input placeholder */
::selection /* User-selected text */
::marker /* List item bullet/number */
::backdrop /* Dialog/fullscreen backdrop */
::-webkit-scrollbar /* Scrollbar (WebKit) */
Conclusion
Pseudo-classes and pseudo-elements give CSS its expressive power. They let you style based on state, position, and user interaction — all without adding extra HTML or JavaScript. Master :has() for parent selection, use ::before/::after for decorative layers, and leverage :focus-visible for accessible interactions. Together, these tools reduce your HTML clutter and keep your CSS declarative and powerful.