Goswami Digital WorldBlog & Insights
All PostsMain SiteContact Us
← All Posts·Web Development

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.

A
Ankur Goswami
25 April 2026 · 12 min read
👁views|
❤️likes|
🔗shares
#css#pseudo-classes#pseudo-elements#selectors#frontend

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.

Next up: CSS Selectors & Specificity — The Complete Guide

Enjoyed this post?
...
Share this post
💬𝕏inF
← Back to BlogWork With Us →