Pseudo-Classes & Pseudo-Elements

Difficulty: Intermediate

Question

What is the difference between pseudo-classes and pseudo-elements?

Answer

Pseudo-classes select elements in a specific STATE (single colon :). Pseudo-elements create virtual ELEMENTS (double colon ::).

Pseudo-classes: - User action: :hover, :focus, :active, :focus-visible - Structural: :first-child, :last-child, :nth-child(), :nth-of-type() - State: :checked, :disabled, :valid, :invalid, :empty - Negation: :not(), :is(), :where(), :has()

Pseudo-elements: - ::before, ::after - insert content before/after - ::first-line, ::first-letter - style text parts - ::placeholder - style input placeholder - ::selection - style highlighted text

Code examples

Pseudo-Classes

/* User action states */
.link:hover { color: blue; }
.link:active { color: red; }
.input:focus { border-color: blue; }
.input:focus-visible {
  outline: 2px solid blue;
  /* Only shows for keyboard focus, not mouse */
}

/* Structural */
li:first-child { font-weight: bold; }
li:last-child { border-bottom: none; }
li:nth-child(odd) { background: #f9f9f9; }
li:nth-child(3n) { color: red; } /* every 3rd */
li:nth-child(n+4) { opacity: 0.7; } /* 4th onwards */

/* Form states */
input:required { border-left: 3px solid red; }
input:valid { border-color: green; }
input:invalid { border-color: red; }
input:disabled { opacity: 0.5; }
checkbox:checked + label { font-weight: bold; }

/* Empty state */
.list:empty::after {
  content: 'No items found';
}

:focus-visible is better than :focus for accessibility - it shows focus rings for keyboard users but not mouse users.

Pseudo-Elements

/* ::before and ::after (must have content) */
.quote::before {
  content: '"';
  font-size: 2rem;
  color: #3b82f6;
}
.required-field::after {
  content: ' *';
  color: red;
}

/* Decorative elements without extra HTML */
.divider::after {
  content: '';
  display: block;
  width: 50px;
  height: 3px;
  background: #3b82f6;
  margin-top: 0.5rem;
}

/* Custom checkbox */
.checkbox::before {
  content: '';
  width: 1.25rem;
  height: 1.25rem;
  border: 2px solid #ccc;
  border-radius: 0.25rem;
  display: inline-block;
}
.checkbox:checked::before {
  background: #3b82f6;
  border-color: #3b82f6;
}

/* Text styling */
p::first-letter {
  font-size: 2rem;
  font-weight: bold;
  float: left;
  margin-right: 0.25rem;
}

/* Selection color */
::selection {
  background: #3b82f6;
  color: white;
}

::before and ::after require the content property (even if empty string). They're inline by default - set display: block or inline-block for sizing.

Modern Pseudo-Classes (:is, :where, :has)

/* :is() - matches any selector in the list */
:is(h1, h2, h3) {
  font-family: serif;
}
/* Same as: h1, h2, h3 { font-family: serif; } */
/* But :is() takes highest specificity in list */

/* :where() - same as :is() but ZERO specificity */
:where(h1, h2, h3) {
  font-family: serif;
  /* Easy to override */
}

/* :has() - parent selector (game changer!) */
.card:has(img) {
  /* Style card only if it contains an image */
  grid-template-rows: 200px 1fr;
}

.form-group:has(input:invalid) {
  /* Style parent when child input is invalid */
  border-color: red;
}

/* :has() for conditional layout */
.grid:has(> :nth-child(4)) {
  /* If grid has 4+ children, use 2 columns */
  grid-template-columns: 1fr 1fr;
}

:has() is the parent selector CSS never had. It styles a parent based on its children. :is() reduces repetition, :where() keeps zero specificity for easy overrides.

Key points

Concepts covered

Pseudo-classes, Pseudo-elements, ::before, ::after, :nth-child, :hover, :focus