Phase 5 · Modern CSS & EffectsModule 22~28 min read

Advanced Selectors & Pseudo-elements

Do more with less JavaScript using structural selectors and generated content.

What you'll learn

CSS selectors go far beyond classes. Structural pseudo-classes target elements by their position, pseudo-elements let you style and generate parts of an element, and modern selectors like :has() can do things that once needed JavaScript.

By the end you'll be able to:

  • Select by position with :nth-child and friends
  • Style pseudo-elements like ::before and ::after
  • Generate content with the content property
  • Use :is(), :where(), and the parent selector :has()

Structural pseudo-classes

These match elements by their place among siblings. :nth-child() is the star — it takes odd, even, a number, or a formula like 3n. Combined with :first-child, :last-child, and the negating :not(), you can style lists and tables precisely without adding a single class:

structural.css
/* Zebra-stripe every other row */
li:nth-child(odd) { background: #f1f5f9; }

/* First and last get special treatment */
li:first-child { font-weight: 700; }
li:last-child  { border-bottom: none; }

/* Everything EXCEPT the last item gets a divider */
li:not(:last-child) { border-bottom: 1px solid #e2e8f0; }
zebra.html
Live preview
Zebra striping with :nth-child(odd) — no extra classes needed.

Pseudo-elements

Where pseudo-classes select whole elements by state, pseudo-elements (written with ::) style a part of an element or inject new content. The most useful: ::before and ::after add generated content, ::selection styles highlighted text, ::placeholder styles input hints, and ::first-line styles the opening line of a paragraph.

Generated content

::before and ::after create a pseudo-element that needs a content property to appear — even if it's just content: ""for a purely decorative shape. Use them for icons, decorative quotes, tooltips, and custom bullets:

content.css
/* Add a star after every .badge */
.badge::after {
  content: " ★";
  color: gold;
}

/* A decorative opening quote */
.quote::before {
  content: "\201C";   /* a big left double-quote */
  font-size: 2em;
  color: #cbd5e1;
}

Watch out

Generated content is decorative — screen readers may ignore it, and users can't select or search it. Never put meaningful text (like a price or a label) in content; keep real content in your HTML and use ::before/::after only for decoration.

is, where & has

Three modern selectors are worth knowing. :is() and :where() shorten long selector lists — :is(h1, h2, h3) a beats writing three rules (they differ only in specificity). And :has() is the long-awaited parent selector: it styles an element based on what it contains — impossible in CSS until recently:

modern.css
/* Style a card ONLY if it contains an image */
.card:has(img) {
  padding-top: 0;
}

/* A form field whose input is invalid */
.field:has(input:invalid) {
  border-color: red;
}

Recap & quick check

Key takeaways

  • :nth-child (odd/even/formulas), :first-child, :last-child, and :not() select by position.
  • Pseudo-elements (::) style parts of an element; ::before and ::after inject content.
  • ::before/::after need a content property to appear — keep them decorative only.
  • :is() and :where() condense long selector lists.
  • :has() is the parent selector — it styles an element based on what it contains.

Quick check

1. Which selector zebra-stripes every other list item?

2. What must ::before have to appear on the page?

3. How many colons do pseudo-elements use?

4. What is special about :has()?

5. Should you put a product's price in a ::before content value?

You can do remarkable things with selectors now. Next we round out modern CSS with the latest layout techniques.