Pseudo-Classes and Pseudo-Elements

Introduction

Pseudo-classes and pseudo-elements let you style state, structure, and generated fragments without adding extra HTML nodes. They are essential for interactive UI feedback, cleaner selectors, and small decorative patterns. This chapter focuses on practical usage and the accessibility guardrails you should follow.

Prerequisites

  • Basic CSS selectors and specificity
  • Familiarity with form controls and interactive elements
  • Basic understanding of focus and accessibility

Pseudo-Class vs Pseudo-Element

Quick distinction:

  • Pseudo-class (:hover, :focus, :nth-child) targets an existing element in a specific state/condition
  • Pseudo-element (::before, ::after, ::marker) targets a conceptual part of an element

You often combine both:

css
.button:hover::after {
  opacity: 1;
}

Common State Pseudo-Classes

:hover

css
.button:hover {
  background: #1d4ed8;
}

Code explanation:

  • applies when pointing device hovers over the element
  • useful for visual affordance on desktop

Do not rely on hover as the only interaction cue (touch devices may not support it consistently).

:active

css
.button:active {
  transform: translateY(1px);
}

Short-lived pressed state feedback for click/tap action.

:focus and :focus-visible

css
.button:focus-visible {
  outline: 3px solid #2563eb;
  outline-offset: 2px;
}

Code explanation:

  • :focus-visible usually shows focus ring for keyboard-style navigation
  • improves accessibility without always showing focus style on mouse click

Prefer :focus-visible for modern UX, with sensible fallback if needed.

css
a:link {
  color: #2563eb;
}
 
a:visited {
  color: #7c3aed;
}
 
a:hover,
a:focus-visible {
  text-decoration-thickness: 2px;
}

Use link states to preserve discoverability and interaction clarity.

Form State Pseudo-Classes

css
input:focus-visible {
  border-color: #2563eb;
}
 
input:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}
 
input:invalid {
  border-color: #dc2626;
}

Code explanation:

  • :disabled styles non-interactive controls
  • :invalid reflects browser constraint validation state
  • combine with labels/help text for clear feedback

Structural Pseudo-Classes

:first-child, :last-child

css
.list li:first-child {
  margin-top: 0;
}
 
.list li:last-child {
  margin-bottom: 0;
}

Useful for trimming edge spacing without extra classes.

:nth-child()

css
.table-row:nth-child(even) {
  background: #f8fafc;
}

Common for zebra striping and repeated rhythm patterns.

:not()

css
.button:not(.is-primary) {
  background: #e2e8f0;
}

Helpful for exclusion logic, but avoid overly complex negation chains.

Useful Modern Selectors

:is()

css
:is(h1, h2, h3) {
  line-height: 1.2;
}

Reduces repetitive grouped selector writing.

:where()

css
:where(article, section) p {
  margin-bottom: 1rem;
}

:where() contributes zero specificity, which is useful for low-priority base rules.

:has() (powerful, use carefully)

css
.field:has(input:invalid) {
  border-color: #dc2626;
}

Lets parent react to child condition. Very expressive, but test browser support in your target matrix.

Pseudo-Elements Overview

Common pseudo-elements:

  • ::before
  • ::after
  • ::marker
  • ::selection
  • ::placeholder

Use double-colon modern syntax for pseudo-elements (::before), though some single-colon legacy forms still work.

::before and ::after

css
.tag::before {
  content: "#";
  margin-right: 0.25rem;
  color: #64748b;
}

Code explanation:

  • generated content appears before element content
  • useful for decorative prefixes/icons
  • requires content property to render

Avoid putting critical semantic content only in generated pseudo-elements.

::marker for List Bullets

css
ul li::marker {
  color: #2563eb;
}

Allows list bullet/number styling without replacing list semantics.

::selection

css
::selection {
  background: #bfdbfe;
  color: #0f172a;
}

Styles user-selected text highlight. Keep contrast readable.

::placeholder

css
input::placeholder {
  color: #94a3b8;
}

Placeholder text should remain readable but visually secondary to user-entered content.

Accessibility Guidance

  • always provide visible :focus-visible for interactive controls
  • do not rely only on color changes for state feedback
  • keep generated pseudo-element content decorative unless redundancy exists
  • ensure hover effects have keyboard equivalents where needed

Warning

Do Not Remove Focus Ring Without Replacement

If you disable default focus visuals, provide a clear custom focus style immediately.

Practical Pattern: Button Interaction States

This gives clear pointer, pressed, and keyboard states with minimal CSS.

Practical Pattern: List Rhythm and Decoration

This pattern adds subtle decoration while preserving semantic list structure.

Common Mistakes

MistakeWhy it hurtsBetter approach
Hover-only feedbackTouch/keyboard users miss cuesAdd focus and active equivalents
Removing focus outlines globallyAccessibility failureUse intentional :focus-visible styles
Complex nested pseudo selectorsHard to debugKeep selectors readable and scoped
Critical content in ::before/::after onlyScreen-reader/semantics riskKeep key meaning in real HTML
Overusing :nth-child for semantic differencesFragile structure couplingUse classes when meaning differs

Debugging Tips

When a pseudo rule does not apply:

  1. confirm selector match in DevTools
  2. verify state is active (:hover, :focus-visible, etc.)
  3. check specificity/order conflicts
  4. for ::before/::after, ensure content exists
  5. test keyboard navigation to validate focus styles

DevTools state toggles are very useful for pseudo-class testing.

Mini Exercise

  1. Style a link list with hover + focus-visible states
  2. Add zebra striping with :nth-child(even)
  3. Add decorative ::before icon for tags
  4. Style form fields for :focus-visible, :invalid, and :disabled
  5. Verify keyboard-only navigation still has clear visual indicators

FAQ

Should I use :focus or :focus-visible?

Use :focus-visible for most interactive UI styling. It usually provides better keyboard-focused behavior.

Is ::before content read by screen readers?

Behavior can vary. Do not rely on generated content for critical meaning or instructions.

Can I style parent elements based on child state?

Yes, with :has() where supported. Test compatibility and keep fallbacks sensible.

Why is my ::before not showing?

Most often because content is missing or empty, or selector does not match.

Is heavy use of pseudo-selectors bad?

Not inherently, but readability and maintainability drop when selectors become overly complex. Keep them intentional and scoped.