Chapter 1 of ?
css 8 min read

CSS3 Mastery — Chapter 12: CSS Pseudo-Classes & Pseudo-Elements

Chapter 12: CSS Pseudo-Classes & Pseudo-Elements

Pseudo-classes target elements based on state, DOM structure, or user interaction, while pseudo-elements let you style specific parts of an element or inject virtual sub-elements without adding HTML tags. Mastering structural selectors, state triggers, relational logic (:has(), :is(), :not()), and pseudo-elements unlocks next-level CSS capabilities.

12.1 Structural Pseudo-Classes (:nth-child, :first-of-type, :last-child)

Structural pseudo-classes select elements based on their position within the parent DOM tree order.

Selector Syntax Pattern Matching Rule
:nth-child(n) :nth-child(2n), :nth-child(odd), :nth-child(3n+1) Matches the n-th child of its parent, regardless of element type.
:first-child / :last-child li:first-child, li:last-child Matches the first or last child element inside a parent container.
:first-of-type / :last-of-type p:first-of-type Matches the first/last occurrence of a specific tag type among siblings.
:only-child span:only-child Matches an element only if it is the sole child inside its parent.
<!DOCTYPE html>
<html>
<head>
  <style>
    *, *::before, *::after { box-sizing: border-box; }
    body { font-family: system-ui, sans-serif; background: #0f172a; color: #f8fafc; padding: 24px; margin: 0; }
    h3 { margin-top: 0; color: #818cf8; }

    .list-demo { list-style: none; padding: 0; margin: 0 0 20px; }
    .list-demo li {
      background: #1e293b; border: 1px solid #334155;
      padding: 10px 16px; margin-bottom: 6px; border-radius: 6px;
      font-size: 13px; color: #94a3b8;
    }

    /* :nth-child(even) zebra striping */
    .zebra li:nth-child(even) { background: rgba(99, 102, 241, 0.2); border-color: #6366f1; color: #a5b4fc; }

    /* :first-child & :last-child */
    .range li:first-child { border-left: 4px solid #10b981; color: #34d399; font-weight: 700; }
    .range li:last-child  { border-left: 4px solid #ef4444; color: #f87171; font-weight: 700; }
  </style>
</head>
<body>
  <h3>Structural Pseudo-Classes Demo</h3>

  <p style="font-size:12px;color:#64748b;">:nth-child(even) — Alternating row highlights:</p>
  <ul class="list-demo zebra">
    <li>Item 1 (odd)</li>
    <li>Item 2 (even — highlighted)</li>
    <li>Item 3 (odd)</li>
    <li>Item 4 (even — highlighted)</li>
  </ul>

  <p style="font-size:12px;color:#64748b;">:first-child (green bar) &amp; :last-child (red bar):</p>
  <ul class="list-demo range">
    <li>First Child Item</li>
    <li>Middle Child Item</li>
    <li>Last Child Item</li>
  </ul>
</body>
</html>

12.2 Form & State Pseudo-Classes (:focus-visible, :checked, :disabled)

State pseudo-classes react dynamically to user input actions, form field validation states, and accessibility focus management.

<!DOCTYPE html>
<html>
<head>
  <style>
    *, *::before, *::after { box-sizing: border-box; }
    body { font-family: system-ui, sans-serif; background: #0f172a; color: #f8fafc; padding: 24px; margin: 0; }
    h3 { margin-top: 0; color: #38bdf8; }

    .form-group { margin-bottom: 16px; max-width: 360px; }
    label { display: block; font-size: 12px; color: #94a3b8; margin-bottom: 6px; }

    input[type="text"] {
      width: 100%; padding: 10px 14px; background: #1e293b;
      border: 1px solid #334155; border-radius: 8px; color: #fff; font-size: 14px;
    }

    /* :focus-visible — keyboard focus ring without mouse click outlines */
    input[type="text"]:focus-visible {
      outline: 2px solid #38bdf8;
      border-color: #38bdf8;
      box-shadow: 0 0 10px rgba(56,189,248,0.3);
    }

    /* :disabled state styling */
    input:disabled {
      background: #0f172a; border-color: #1e293b; opacity: 0.5; cursor: not-allowed;
    }

    /* Custom Checkbox using :checked + label */
    .checkbox-label { display: flex; align-items: center; gap: 10px; cursor: pointer; user-select: none; }
    .checkbox-label input[type="checkbox"] { display: none; }
    .custom-box {
      width: 20px; height: 20px; border: 2px solid #6366f1; border-radius: 4px;
      display: flex; align-items: center; justify-content: center; transition: all 0.2s;
    }
    /* When input is :checked, style adjacent .custom-box */
    input[type="checkbox"]:checked + .custom-box {
      background: #6366f1; border-color: #6366f1;
    }
    input[type="checkbox"]:checked + .custom-box::after {
      content: "✔"; color: #fff; font-size: 12px; font-weight: bold;
    }
  </style>
</head>
<body>
  <h3>Form State Pseudo-Classes</h3>

  <div class="form-group">
    <label>Focus field (:focus-visible ring):</label>
    <input type="text" placeholder="Click or Tab here..." />
  </div>

  <div class="form-group">
    <label>Disabled Input (:disabled):</label>
    <input type="text" value="Disabled input field" disabled />
  </div>

  <div class="form-group">
    <label class="checkbox-label">
      <input type="checkbox" checked />
      <div class="custom-box"></div>
      <span style="font-size:14px;color:#f8fafc;">Custom Checkbox (:checked state)</span>
    </label>
  </div>
</body>
</html>

12.3 Relational & Logic Pseudo-Classes (:not, :is, :where, :has())

CSS3 & CSS4 introduced powerful logic functions that reduce selector duplication and enable parent styling based on child elements.

Logic Selector Syntax & Example Key Benefit
:not(selector) button:not(.primary) Selects all elements EXCEPT those matching the argument.
:is(s1, s2) :is(h1, h2, h3) span Groups multiple selectors into a single line; takes specificity of highest argument.
:where(s1, s2) :where(h1, h2, h3) span Identical to :is() but has 0 specificity (easy to override in design systems).
:has(selector) .card:has(img) The Parent Selector! Styles a parent element if it contains a matching descendant.
<!DOCTYPE html>
<html>
<head>
  <style>
    *, *::before, *::after { box-sizing: border-box; }
    body { font-family: system-ui, sans-serif; background: #0f172a; color: #f8fafc; padding: 24px; margin: 0; }
    h3 { margin-top: 0; color: #f59e0b; }

    .card {
      background: #1e293b; border: 2px solid #334155; border-radius: 10px;
      padding: 16px; margin-bottom: 12px; max-width: 400px; transition: all 0.3s;
    }

    /* :has() — Style parent card if it contains a featured badge */
    .card:has(.badge-featured) {
      border-color: #f59e0b;
      background: rgba(245, 158, 11, 0.1);
      box-shadow: 0 4px 16px rgba(245, 158, 11, 0.2);
    }

    .badge-featured {
      display: inline-block; background: #f59e0b; color: #0f172a;
      padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 800;
    }

    /* :not() — Style all buttons EXCEPT .btn-danger */
    .btn { background: #3b82f6; color: #fff; border: none; padding: 8px 14px; border-radius: 6px; margin-right: 6px; cursor: pointer; }
    .btn:not(.btn-danger):hover { background: #60a5fa; }
    .btn-danger { background: #ef4444; }
  </style>
</head>
<body>
  <h3>Logic Pseudo-Classes — :has() &amp; :not()</h3>

  <p style="font-size:12px;color:#64748b;">:has() — Card 2 is highlighted because it contains .badge-featured:</p>
  <div class="card">Standard Card (no featured badge)</div>
  <div class="card">
    <span class="badge-featured">FEATURED</span>
    <p style="margin:6px 0 0;font-size:13px;color:#cbd5e1;">This parent card automatically turned amber because of <code>.card:has(.badge-featured)</code>!</p>
  </div>

  <p style="font-size:12px;color:#64748b;margin-top:16px;">:not() — Hover both buttons (only non-danger gets light blue hover):</p>
  <button class="btn">Normal Button</button>
  <button class="btn btn-danger">Danger Button (:not excluded)</button>
</body>
</html>

12.4 Pseudo-Elements (::before, ::after, ::placeholder, ::selection)

Pseudo-elements style specific parts of an element or create virtual sub-elements without altering your HTML markup.

Pseudo-Element Target Effect Common Use Case
::before Injects virtual element before content Icons, decorative quote marks, custom bullets.
::after Injects virtual element after content Clearfix clears, tooltips, animated underline bars.
::placeholder Styles input placeholder text Customizing placeholder font color and opacity.
::selection Styles text highlighted by mouse drag Customizing highlight selection background & text color.
::first-letter Styles the very first letter Creating editorial drop caps in articles.
<!DOCTYPE html>
<html>
<head>
  <style>
    *, *::before, *::after { box-sizing: border-box; }
    body { font-family: system-ui, sans-serif; background: #0f172a; color: #f8fafc; padding: 24px; margin: 0; }
    h3 { margin-top: 0; color: #10b981; }

    /* ::selection — Custom text highlight color */
    ::selection {
      background: #6366f1;
      color: #ffffff;
    }

    /* ::first-letter — Drop cap editorial style */
    .article-intro::first-letter {
      font-size: 3rem;
      float: left;
      line-height: 0.8;
      margin-right: 8px;
      color: #10b981;
      font-weight: 900;
    }

    /* ::placeholder styling */
    input::placeholder {
      color: #64748b;
      font-style: italic;
    }
    input {
      padding: 10px 14px; background: #1e293b; border: 1px solid #334155;
      border-radius: 6px; color: #fff; width: 100%; max-width: 320px;
    }

    /* ::after animated underline link */
    .fancy-link {
      color: #38bdf8; text-decoration: none; font-weight: 700;
      position: relative; display: inline-block; padding-bottom: 4px;
    }
    .fancy-link::after {
      content: "";
      position: absolute; bottom: 0; left: 0;
      width: 0; height: 2px;
      background: #38bdf8;
      transition: width 0.3s ease;
    }
    .fancy-link:hover::after {
      width: 100%;
    }
  </style>
</head>
<body>
  <h3>Pseudo-Elements Demo</h3>

  <p style="font-size:12px;color:#64748b;">Highlight text below with your mouse (custom ::selection color!):</p>
  <p class="article-intro" style="font-size:14px;color:#cbd5e1;line-height:1.6;">
    Cascading Style Sheets allow developers to inject virtual pseudo-elements without polluting HTML DOM markup with extra spans or divs.
  </p>

  <p style="font-size:12px;color:#64748b;margin-top:16px;">::placeholder styling:</p>
  <input type="text" placeholder="Custom italic placeholder..." />

  <p style="font-size:12px;color:#64748b;margin-top:16px;">::after animated underline on hover:</p>
  <a href="#" class="fancy-link">Hover over this link for underline animation &rarr;</a>
</body>
</html>

12.5 Component Project: Interactive Task Card with :has() State Management

This component uses the relational parent pseudo-class :has() to dynamically restyle the entire parent card when a child checkbox is checked — with zero JavaScript:

<!DOCTYPE html>
<html>
<head>
  <style>
    *, *::before, *::after { box-sizing: border-box; }
    body { font-family: system-ui, sans-serif; background: #0f172a; color: #f8fafc; padding: 24px; margin: 0; }

    .task-card {
      background: #1e293b; border: 2px solid #334155; border-radius: 12px;
      padding: 20px; max-width: 400px; transition: all 0.3s ease;
    }

    /* Parent styling based on child state */
    .task-card:has(input:checked) {
      border-color: #10b981; background: rgba(16, 185, 129, 0.15);
    }
    .task-card:has(input:checked) .task-title {
      text-decoration: line-through; color: #64748b;
    }
    .task-card:has(input:checked) .badge {
      background: #10b981; color: #fff;
    }

    .task-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
    .task-title { font-weight: 700; font-size: 15px; margin: 0; color: #f8fafc; transition: all 0.3s; }
    .badge { background: #334155; color: #94a3b8; font-size: 11px; font-weight: 800; padding: 4px 10px; border-radius: 999px; transition: all 0.3s; }
    
    .checkbox-label { display: flex; align-items: center; gap: 10px; cursor: pointer; font-size: 13px; color: #cbd5e1; }
    .checkbox-label input { width: 18px; height: 18px; accent-color: #10b981; cursor: pointer; }
  </style>
</head>
<body>
  <div class="task-card">
    <div class="task-header">
      <h4 class="task-title">Deploy Production Release</h4>
      <span class="badge">PENDING</span>
    </div>
    <label class="checkbox-label">
      <input type="checkbox" /> Mark task as complete (styler via :has())
    </label>
  </div>
</body>
</html>

12.6 Interactive Sandbox — Pseudo-Classes & Pseudo-Elements

Experiment live with :nth-child, :has() parent selection, ::selection highlights, and ::after hover bars in the playground below:

Live Playground — Chapter 12 Sandbox
Done with this chapter?
Mark it complete to track your progress and unlock your certificate.
Next Up

Learner Reviews

Write a Review
Share your experience to help other learners.
Your Rating *