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-childand friends - Style pseudo-elements like
::beforeand::after - Generate content with the
contentproperty - 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:
/* 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; }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:
/* 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
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:
/* 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.