Combinators & Pseudo-classes
Basic selectors pick elements by name, class or attribute. Real pages need more: "links inside the menu", "every second table row", "the button the user is pointing at". Combinators and pseudo-classes give you that power without adding extra classes to your HTML.
In this lesson you will learn the four combinators, the most useful pseudo-classes, and pseudo-elements, which let you style parts of an element.
Combinators
A combinator joins two selectors and describes how the elements are related in the HTML tree.
| Combinator | Example | Matches |
|---|---|---|
| Descendant (a space) | nav a | any <a> anywhere inside a <nav> |
Child > | ul > li | <li> elements that are direct children of a <ul> |
Next sibling + | h2 + p | a <p> that comes straight after an <h2> |
Subsequent sibling ~ | h2 ~ p | every <p> after an <h2> with the same parent |
The difference between descendant and child matters when lists are nested:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Combinators</title>
<style>
body { font-family: system-ui, sans-serif; padding: 1rem; }
.menu > li { font-weight: bold; } /* top level only */
.menu li li { font-weight: normal; color: #5f6b7a; } /* items inside items */
h2 + p { font-size: 1.15rem; color: #1d3557; } /* first paragraph after a heading */
</style>
</head>
<body>
<ul class="menu">
<li>Courses
<ul>
<li>B.Com</li>
<li>BCA</li>
</ul>
</li>
<li>Admissions</li>
</ul>
<h2>Hostel rules</h2>
<p>Gates close at 10 pm.</p>
<p>Visitors may meet students in the common room only.</p>
</body>
</html>.menu > li targets only the top-level items. The inner items would still look bold, because font-weight is inherited from their bold parent <li>, so .menu li li sets them back to normal and greys them. The h2 + p rule styles only the first paragraph after the heading, a common way to make an introduction stand out.
User action pseudo-classes
A pseudo-class starts with one colon and matches an element when it is in a certain state. The state can change while the page is open, and the styles follow automatically.
:hovermatches while the mouse pointer is over the element.:activematches while the element is being pressed.:focusmatches when the element has keyboard focus, for example after the user tabs to it.:focus-visiblematches when the element has focus and the browser decides a visible ring is helpful, which is mainly when the user is using a keyboard.
.btn:hover {
background: #0a4d8c;
}
.btn:active {
transform: translateY(1px);
}
.btn:focus-visible {
outline: 3px solid #f4a300;
outline-offset: 2px;
}Use :focus-visible for focus rings. Keyboard users get a clear outline, and mouse users do not see a ring after every click. Never remove the focus outline without providing a replacement: people who cannot use a mouse depend on it to know where they are.
For links you also have :link (not yet visited) and :visited. For privacy reasons, browsers only let :visited change colours (such as color and background-color), not sizes, fonts or images. If you style several link states, write them in the order :link, :visited, :hover, :active, so the later states win when more than one applies.
Structural pseudo-classes
Structural pseudo-classes pick elements by their position among their siblings.
:first-childand:last-childmatch the first or last child of its parent.:only-childmatches an element with no siblings.:nth-child(n)matches by position. It accepts a number (3), the keywordsoddandeven, or a formula like3n(every third) or3n + 1(1st, 4th, 7th...).:nth-of-type(n)counts only siblings of the same element type.:emptymatches an element with no children and no text.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Structural pseudo-classes</title>
<style>
body { font-family: system-ui, sans-serif; padding: 1rem; }
li { padding: 0.4rem 0.6rem; list-style: none; }
li:nth-child(even) { background: #eef3fa; }
li:first-child { font-weight: bold; }
li:last-child { border-bottom: 2px solid #1d3557; }
li:not(.done) { color: #8a1c1c; }
</style>
</head>
<body>
<h2>Rohan's study plan</h2>
<ul>
<li class="done">Chapter 1: Real numbers</li>
<li class="done">Chapter 2: Polynomials</li>
<li>Chapter 3: Linear equations</li>
<li>Chapter 4: Quadratic equations</li>
<li>Chapter 5: Arithmetic progressions</li>
</ul>
</body>
</html>Every even row gets a light background, the first item is bold and the last has a line under it. The :not(.done) pseudo-class matches items that do not have the class done, so unfinished chapters turn red. :not() accepts a list, so :not(.done, .skipped) excludes both.
Pseudo-elements
A pseudo-element starts with two colons and styles a part of an element, or adds a decorative part that is not in the HTML.
::beforeand::afterinsert a box at the start or end of the element's content. They need thecontentproperty, even if it is empty (content: "").::first-letterand::first-linestyle the first letter or line of a block of text.::placeholderstyles the hint text in a form field.::selectionstyles text the user has highlighted.::markerstyles list bullets and numbers (see Styling Lists & Tables).
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Pseudo-elements</title>
<style>
body { font-family: Georgia, serif; padding: 1rem; max-width: 40rem; }
.story::first-letter {
font-size: 2.5rem;
font-weight: bold;
color: #8a3324;
}
.required::after { content: " *"; color: #c62828; }
::selection { background: #ffe08a; }
</style>
</head>
<body>
<p class="story">Every evening, Ananya walked to the ghat in Varanasi with her grandfather. He told her a new story about the river each day, and she wrote them all down in a small blue notebook.</p>
<p class="required">Your name</p>
</body>
</html>The story starts with a large red first letter, and the label gets a red star from ::after. Text added with content is decoration: screen readers may or may not read it, and users cannot select it, so never put important information there.
Common mistakes
- Using one colon for pseudo-elements.
::beforeis the modern form. Single-colon:beforestill works for the oldest four (before,after,first-letter,first-line) only for backwards compatibility. - Forgetting
content. A::beforerule withoutcontentcreates nothing at all. - Confusing
:nth-childand:nth-of-type.p:nth-child(2)matches a paragraph only if it is the second child of any type. If the first child is an<h2>, the first paragraph is child 2. - Hover-only interactions. Phones have no hover. Make sure everything a hover reveals is also reachable by tap and keyboard.
Key points
- Combinators describe relationships: space (descendant),
>(child),+(next sibling),~(later siblings). - Pseudo-classes such as
:hover,:focus-visibleand:nth-child()match elements by state or position. - Use
:focus-visiblefor clear keyboard focus rings, and never remove focus styles without a replacement. :not()excludes elements and accepts a selector list.- Pseudo-elements such as
::before,::afterand::first-letterstyle parts of elements;::beforeand::afterneedcontent.
Exercise
Build a train timetable as a <ul> with six stations from Jaipur to Delhi. Using only pseudo-classes and combinators (no extra classes), make the first and last stations bold, give every odd row a light background, and add the text " (stop)" after every item using ::after. Finally, make an item turn dark blue when the mouse is over it.
Show solution
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Train stops</title>
<style>
body { font-family: system-ui, sans-serif; }
ul { list-style: none; padding: 0; }
li { padding: 0.4rem; }
li:nth-child(odd) { background: #f1f4f8; }
li:first-child,
li:last-child { font-weight: bold; }
li::after { content: " (stop)"; color: #666; }
li:hover { color: #0b3d91; }
</style>
</head>
<body>
<ul>
<li>Jaipur</li>
<li>Gandhinagar Jaipur</li>
<li>Bandikui</li>
<li>Alwar</li>
<li>Rewari</li>
<li>Delhi</li>
</ul>
</body>
</html>The selector list li:first-child, li:last-child shares one declaration between the two end stations.
Quick Quiz
1. Which selector matches only <li> elements that are direct children of .menu?
The > combinator selects direct children only; a space selects descendants at any depth.
2. Which pseudo-class is best for showing a focus ring mainly to keyboard users?
:focus-visible matches focused elements when the browser decides a visible indicator helps, such as keyboard navigation.
3. What does li:nth-child(3n + 1) match?
With n = 0, 1, 2..., the formula gives positions 1, 4, 7 and so on.
4. Why does .icon::before { color: red; } show nothing?
::before and ::after are only created when content is set, even to an empty string.
Learn CSS with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
