Combinators & Pseudo-classes

CSS · Lesson 4 of 55 7 min read Updated

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.

CombinatorExampleMatches
Descendant (a space)nav aany <a> anywhere inside a <nav>
Child >ul > li<li> elements that are direct children of a <ul>
Next sibling +h2 + pa <p> that comes straight after an <h2>
Subsequent sibling ~h2 ~ pevery <p> after an <h2> with the same parent

The difference between descendant and child matters when lists are nested:

HTMLTry it yourself
<!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.

  • :hover matches while the mouse pointer is over the element.
  • :active matches while the element is being pressed.
  • :focus matches when the element has keyboard focus, for example after the user tabs to it.
  • :focus-visible matches when the element has focus and the browser decides a visible ring is helpful, which is mainly when the user is using a keyboard.
CSS
.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-child and :last-child match the first or last child of its parent.
  • :only-child matches an element with no siblings.
  • :nth-child(n) matches by position. It accepts a number (3), the keywords odd and even, or a formula like 3n (every third) or 3n + 1 (1st, 4th, 7th...).
  • :nth-of-type(n) counts only siblings of the same element type.
  • :empty matches an element with no children and no text.
HTMLTry it yourself
<!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.

  • ::before and ::after insert a box at the start or end of the element's content. They need the content property, even if it is empty (content: "").
  • ::first-letter and ::first-line style the first letter or line of a block of text.
  • ::placeholder styles the hint text in a form field.
  • ::selection styles text the user has highlighted.
  • ::marker styles list bullets and numbers (see Styling Lists & Tables).
HTMLTry it yourself
<!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. ::before is the modern form. Single-colon :before still works for the oldest four (before, after, first-letter, first-line) only for backwards compatibility.
  • Forgetting content. A ::before rule without content creates nothing at all.
  • Confusing :nth-child and :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-visible and :nth-child() match elements by state or position.
  • Use :focus-visible for 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, ::after and ::first-letter style parts of elements; ::before and ::after need content.

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
HTML
<!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?

2. Which pseudo-class is best for showing a focus ring mainly to keyboard users?

3. What does li:nth-child(3n + 1) match?

4. Why does .icon::before { color: red; } show nothing?

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