DNAðŸŽĻ CSSPseudo-Classes vs Pseudo-Elements
ðŸĢHatchlingCSSSelectorsPseudo-ClassesPseudo-Elements

Pseudo-Classes vs Pseudo-Elements

Pseudo-classes select elements by state or position. Pseudo-elements create virtual elements for styling. The single-colon vs double-colon distinction is just the beginning.

Pseudo-Classes vs Pseudo-Elements

The distinction between pseudo-classes and pseudo-elements is one of the most commonly confused CSS topics. They use similar syntax, both extend selector capabilities, but they serve fundamentally different purposes.

The Core Distinction

Pseudo-class:   Selects an EXISTING element in a particular STATE
Pseudo-element: Creates a VIRTUAL element that doesn't exist in the DOM
FeaturePseudo-ClassPseudo-Element
SyntaxSingle colon :Double colon ::
PurposeSelect by state/positionCreate virtual content
DOM elementAlready existsDoesn't exist in DOM
QuantityUnlimited per selectorLimited (few per element)
Example:hover, :first-child::before, ::after

Pseudo-Classes

User Action States

a:hover  { color: blue; }       /* Mouse is over element */
a:active { color: red; }        /* Element is being clicked */
a:focus  { outline: 2px solid; } /* Element has keyboard focus */
a:focus-visible { outline: 2px solid blue; }  /* Focus via keyboard only */
a:focus-within  { border-color: blue; }       /* A descendant has focus */
 
a:visited { color: purple; }    /* Link has been visited */
a:link    { color: blue; }      /* Unvisited link */

:focus vs :focus-visible: :focus fires on all focus (including mouse clicks). :focus-visible fires only when focus should be visually indicated (keyboard navigation). Use :focus-visible for outlines to avoid showing outlines on mouse clicks:

button:focus { outline: none; }         /* Remove default */
button:focus-visible { outline: 2px solid blue; }  /* Show for keyboard */

Structural Pseudo-Classes

li:first-child { }    /* First child of its parent */
li:last-child  { }    /* Last child of its parent */
li:nth-child(2) { }   /* Second child */
li:nth-child(odd) { }  /* 1st, 3rd, 5th... */
li:nth-child(even) { } /* 2nd, 4th, 6th... */
li:nth-child(3n) { }   /* Every 3rd (3, 6, 9...) */
li:nth-child(3n+1) { } /* 1st of every 3 (1, 4, 7...) */
li:nth-last-child(2) { } /* Second from last */
li:only-child { }      /* Only child of its parent */
 
p:first-of-type { }   /* First <p> among siblings */
p:last-of-type  { }   /* Last <p> among siblings */
p:nth-of-type(2) { }  /* Second <p> among siblings */
p:only-of-type  { }   /* Only <p> among siblings */

:nth-child vs :nth-of-type:

<div>
  <h2>Title</h2>     <!-- h2:first-child, h2:first-of-type -->
  <p>First para</p>  <!-- p:nth-child(2), p:first-of-type -->
  <p>Second para</p> <!-- p:nth-child(3), p:nth-of-type(2) -->
</div>

:nth-child(2) selects the second child regardless of type. :nth-of-type(2) selects the second element of that specific type.

Form State Pseudo-Classes

input:required   { border-left: 3px solid blue; }
input:optional   { border-left: 3px solid gray; }
input:valid      { border-color: green; }
input:invalid    { border-color: red; }
input:disabled   { opacity: 0.5; cursor: not-allowed; }
input:enabled    { }
input:checked    { }
input:placeholder-shown { }
input:read-only  { background: #f5f5f5; }
input:in-range   { }  /* Number input within min/max */
input:out-of-range { color: red; }
input:autofill   { box-shadow: 0 0 0 1000px white inset; }

Modern Pseudo-Classes

/* :is() — matches any argument, takes highest specificity */
:is(h1, h2, h3):hover { color: blue; }
 
/* :where() — same as :is() but ZERO specificity */
:where(h1, h2, h3) { margin-bottom: 1rem; }
 
/* :has() — parent selector (selects element based on descendants) */
.card:has(img)       { grid-template-rows: 200px 1fr; }
.card:has(:invalid)  { border-color: red; }
form:has(:focus-within) { box-shadow: 0 0 0 2px blue; }
 
/* :not() — negation */
input:not(:disabled):not(:read-only) { cursor: text; }
 
/* :empty — element with no children (not even whitespace in modern browsers) */
.message:empty { display: none; }

Pseudo-Elements

::before and ::after

Create virtual elements as the first/last child:

.required-field::after {
  content: ' *';
  color: red;
}
 
.external-link::after {
  content: ' ↗';
  font-size: 0.75em;
}
 
.quote::before {
  content: open-quote;
}
 
.quote::after {
  content: close-quote;
}

Critical rule: ::before and ::after require the content property (even if empty):

/* Won't render — missing content */
.element::before {
  width: 20px;
  height: 20px;
  background: red;
}
 
/* Will render */
.element::before {
  content: '';
  display: block;
  width: 20px;
  height: 20px;
  background: red;
}

Common ::before / ::after Patterns

/* Decorative line */
.section-title::after {
  content: '';
  display: block;
  width: 60px;
  height: 3px;
  background: var(--color-primary);
  margin-top: 0.5rem;
}
 
/* Overlay on images */
.card-image::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(transparent 50%, rgba(0,0,0,0.8));
}
 
/* Custom checkbox */
.checkbox::before {
  content: '';
  width: 20px;
  height: 20px;
  border: 2px solid #ccc;
  border-radius: 4px;
}
 
.checkbox:checked::before {
  background: var(--color-primary);
  border-color: var(--color-primary);
}
 
/* Counter */
ol {
  counter-reset: step;
}
 
li::before {
  counter-increment: step;
  content: counter(step) '. ';
  font-weight: bold;
  color: var(--color-primary);
}

Other Pseudo-Elements

/* First line of a block */
p::first-line {
  font-weight: bold;
  text-transform: uppercase;
}
 
/* First letter (drop cap) */
p::first-letter {
  font-size: 3em;
  float: left;
  line-height: 1;
  margin-right: 0.1em;
}
 
/* Text selection highlight */
::selection {
  background: var(--color-primary);
  color: white;
}
 
/* Placeholder text */
input::placeholder {
  color: #999;
  font-style: italic;
}
 
/* Scrollbar styling (Webkit) */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: #f1f1f1; }
::-webkit-scrollbar-thumb { background: #888; border-radius: 4px; }
 
/* List marker */
li::marker {
  color: var(--color-primary);
  font-size: 1.2em;
}
 
/* File input button */
input[type="file"]::file-selector-button {
  padding: 8px 16px;
  border-radius: 4px;
  background: var(--color-primary);
  color: white;
}

Specificity Comparison

:hover           /* (0,0,1,0) — one pseudo-class */
::before         /* (0,0,0,1) — one pseudo-element */
:first-child     /* (0,0,1,0) — one pseudo-class */
::first-line     /* (0,0,0,1) — one pseudo-element */
:is(.a, #b)      /* (0,1,0,0) — takes highest argument (the #b) */
:where(.a, #b)   /* (0,0,0,0) — always zero */
:not(.active)    /* (0,0,1,0) — specificity of argument */
:has(.checked)   /* (0,0,1,0) — specificity of argument */

Pseudo-classes contribute to the class/attribute column. Pseudo-elements contribute to the element column.

Accessibility Note

::before and ::after content:

  • Is read by most screen readers (but inconsistently)
  • Should not contain essential information
  • Use content: '' / '' for decorative elements
  • For critical content, use real DOM elements with appropriate ARIA
/* Decorative — screen reader should ignore */
.icon::before {
  content: '★';
  /* Add aria-hidden on the parent or use an actual <span aria-hidden="true"> */
}

Interview Signal

Senior candidates demonstrate:

  1. Clear distinction — Pseudo-classes select by state, pseudo-elements create virtual nodes; single vs double colon
  2. :nth-child formulas — Can explain an+b syntax, knows the difference between :nth-child and :nth-of-type
  3. :focus-visible — Knows why it exists (keyboard vs mouse focus indication)
  4. ::before/::after mastery — Requires content, common patterns (decorative lines, overlays, counters)
  5. Modern selectors — :has() as parent selector, :is() vs :where() specificity difference, :empty for conditional display