HTML and CSS course Β· Module 3: CSS Basics

CSS Pseudo-classes and Pseudo-elements

7 min read Β·
In this lesson4

You want a link to turn gold on hover, every other table row to get a different background, and a hieroglyph before each heading - without extra classes or decorations in the HTML. For this, CSS has pseudo-classes and pseudo-elements.

Mohamed points at the temple paintings: "See how the colors change depending on the light? In CSS, we have pseudo-classes and pseudo-elements that allow changing the appearance of elements depending on their state or adding decorations without changing the HTML!"

Pseudo-classes - Reacting to State

Pseudo-classes allow styling elements depending on their state or position in the document. We write them with a single colon :.

Interactive States

The first group reacts to what the user does: hovering, clicking, or tabbing into a form field:

/* Visited link */
a:visited {
  color: #8B7355;
}

/* When cursor is over element */
a:hover {
  color: gold;
  text-decoration: underline;
}

/* When element is active (clicked) */
button:active {
  background-color: darkgoldenrod;
  transform: scale(0.95);
}

/* When element has focus (e.g., after clicking Tab) */
input:focus {
  border-color: gold;
  outline: 2px solid rgba(255, 215, 0, 0.5);
}

The order matters: a:visited comes before a:hover. Both rules are equally strong, so the later one wins - if :visited came lower, a visited link wouldn't turn gold on hover. MDN recommends the order :link, :visited, :hover, :active. Also, never remove the :focus outline without another clear style - keyboard users rely on it.

Structural Pseudo-classes

The second group selects elements by their position in the document tree - first, last, or every other one:

/* First child element */
li:first-child {
  font-weight: bold;
  color: gold;
}

/* Last child element */
li:last-child {
  border-bottom: 2px solid gold;
}

/* Every other element (even) */
tr:nth-child(even) {
  background-color: #f5f0e6;
}

/* Every other element (odd) */
tr:nth-child(odd) {
  background-color: #fff;
}

/* Every third element */
li:nth-child(3n) {
  color: crimson;
}

/* Third element from end */
li:nth-last-child(3) {
  font-style: italic;
}

Elements are counted from 1, not from 0, so nth-child(3n) selects the third, sixth and ninth element, while even and odd mean even and odd. From the earliest, the order is: :first-child, :nth-child(2), :nth-child(3), and finally :last-child.

Modern Pseudo-classes

The third group holds newer tools that shorten long selectors and select elements by what they contain:

/* Negation - all links EXCEPT .special class */
a:not(.special) {
  color: #8B6914;
}

/* Grouping selectors - :is() */
:is(h1, h2, h3):hover {
  color: gold;
}

/* :has() - parent selector (newest!) */
/* Style article THAT CONTAINS an image */
article:has(img) {
  border: 2px solid gold;
  padding: 20px;
}

/* Div that contains empty paragraph */
div:has(p:empty) {
  display: none;
}

The :not() pseudo-class reverses a condition, :is() gathers several selectors into one, and :has() selects a parent based on its content - article:has(img) styles the article, not the image. All major browsers have supported :has() since the end of 2023.

Pseudo-elements - Decorations Without HTML

Pseudo-elements allow styling parts of an element or adding decorative content without changing HTML. We write them with double colons ::. Browsers also understand the old single-colon notation, e.g. :before, but I recommend always writing :: to tell them apart from pseudo-classes.

::before and ::after

They add content before or after the element's content, inside the element. They require the content property: without it, the pseudo-element isn't created at all, even with a color and dimensions.

/* Add hieroglyph before heading */
h2::before {
  content: "π“‚€ ";
  color: gold;
}

/* Add arrow after link */
a::after {
  content: " β†’";
  color: gold;
}

/* Decorative line under heading */
h1::after {
  content: "";
  display: block;
  width: 100px;
  height: 3px;
  background: linear-gradient(to right, gold, transparent);
  margin-top: 10px;
}

In the third example, content: "" is an empty string, but it's enough to create the pseudo-element - dimensions and the gradient do the rest. A full rule is a selector, a pseudo-element and a block with content, e.g. h1::after { content: "β†’"; }. This content isn't in the HTML, so don't put anything important there.

::placeholder and ::selection

These two pseudo-elements style the hint in an empty form field and text selected with the mouse - fragments you can't point to with any tag.

/* Placeholder style in input */
input::placeholder {
  color: #c4a35a;
  font-style: italic;
}

/* Selected text style */
p::selection {
  background-color: gold;
  color: #2c1810;
}

The placeholder disappears when the user starts typing, so it doesn't replace the field's label.

::first-line and ::first-letter

The last pair works on a paragraph's text - its first line or first letter, like a decorative initial in a scribe's manuscript.

/* First letter of paragraph - initial effect */
p::first-letter {
  font-size: 3em;
  float: left;
  color: gold;
  font-family: serif;
  line-height: 1;
  margin-right: 5px;
}

/* First line of text */
p::first-line {
  font-weight: bold;
  color: #8B6914;
}

The float: left declaration and a large font-size create an initial that is several lines of text tall.

Practical Applications

Now let's combine both families in real components.

Interactive List with Hover Effects

The first component is a list of pharaohs whose rows get a background and a golden left border on hover:

.pharaoh-list li {
  padding: 10px;
  transition: all 0.3s ease;
  border-left: 3px solid transparent;
}

.pharaoh-list li:hover {
  background-color: #f5f0e6;
  border-left-color: gold;
  padding-left: 20px;
}

.pharaoh-list li:nth-child(even) {
  background-color: #faf8f3;
}

.pharaoh-list li::before {
  content: "Β» ";
  color: gold;
}

The transition declaration makes the change smooth - you'll learn transitions in the module on animations. The ::before rule has an empty content for now, so it shows nothing - in the exercise you'll add an arrow.

Buttons with Pseudo-elements

The second component is a button across which a band of light slides on hover, like a ray of sunlight across a golden mask:

.btn-egyptian {
  position: relative;
  padding: 12px 24px;
  background: #8B6914;
  color: white;
  border: none;
  overflow: hidden;
}

.btn-egyptian::before {
  content: "";
  position: absolute;
  top: 0;
  left: -100%;
  width: 100%;
  height: 100%;
  background: linear-gradient(
    90deg, transparent, rgba(255,215,0,0.3), transparent
  );
  transition: left 0.5s;
}

.btn-egyptian:hover::before {
  left: 100%;
}

The ::before pseudo-element is a gradient band hidden outside the button (left: -100%), which moves to the other side on hover. Note the :hover::before notation - first the pseudo-class, then the pseudo-element.

Tooltips with CSS

The third component is a tooltip whose text the attr() function takes from the data-tooltip attribute in the HTML:

.tooltip {
  position: relative;
}

.tooltip::after {
  content: attr(data-tooltip);
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  background: #2c1810;
  color: gold;
  padding: 5px 10px;
  border-radius: 4px;
  font-size: 14px;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s;
}

.tooltip:hover::after {
  opacity: 1;
}

The tooltip is invisible (opacity: 0) and fades in on hover, while pointer-events: none keeps it from catching clicks.

Debugging in DevTools

When a style doesn't work, don't guess. Right-click the element, choose Inspect, find the rules that apply to it in the Styles panel, compare selector specificity and fix the value in the inspector. The :hov button forces the :hover state without moving the mouse. Then copy the working fix into the CSS file, because inspector changes vanish on refresh.

Mohamed summarizes: "Pseudo-classes react to element state, pseudo-elements add decorations. Together they create dynamic interfaces without additional HTML!"

In the editor below, all these techniques work together - hover over the pharaohs' names and click the form fields:

Code for this lesson: index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Pseudo-classes and pseudo-elements</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <h1>Pharaohs of Egypt</h1>

  <ul class="pharaoh-list">
    <li data-tooltip="2589-2566 BCE">Cheops (Chufu)</li>
    <li data-tooltip="1332-1323 BCE">Tutankhamun</li>
    <li data-tooltip="1279-1213 BCE">Ramesses II</li>
    <li data-tooltip="51-30 BCE">Cleopatra VII</li>
    <li data-tooltip="1479-1458 BCE">Hatshepsut</li>
  </ul>

  <h2>Contact the museum</h2>
  <form>
    <label for="name">Your name</label>
    <input type="text" id="name" placeholder="Your name...">
    <label for="email">Your email</label>
    <input type="email" id="email" placeholder="Your email...">
    <button class="btn-egyptian">Send message</button>
  </form>

  <article>
    <p>Ancient Egypt is one of the oldest civilizations in the world. For thousands of years, Egyptians built monumental structures, developed a writing system, and created an advanced culture that fascinates us to this day.</p>
  </article>
</body>
</html>

Remember: a pseudo-class with one colon reacts to an element's state, and a pseudo-element with two colons adds a decoration to it - like the light changing the colors of the paintings and a scribe's ornate initial.

Docs entries used in this lesson

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. Which pseudo-class lets you style an element when the mouse cursor is over it?

  2. 2. What is required for a ::before or ::after pseudo-element to be visible?

These are 2 of 3 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    Uncomment and complete the CSS rules: hover, first-child, nth-child(even), ::before, :focus, ::placeholder, button:hover

  • Horizontal ordering

    Arrange the elements to create a correct CSS rule with the ::after pseudo-element:

  • Click in order

    Arrange the :nth-child selectors from selecting the earliest to the latest elements:

  • Code editor

    Uncomment and complete the CSS rules with pseudo-elements: ::before on h1 and h2, ::after as a decorative line, ::first-letter as a drop cap, ::selection

  • Vertical ordering

    Arrange the steps for debugging a CSS styling issue:

  • Vertical ordering

    Arrange the steps for resolving a CSS style conflict (when an element looks unexpected):

  • Code editor

    Build a web page about Egyptian gods. Use CSS for styling: colors, fonts, box model, display modes (block, inline, inline-block), and descendant selectors.

Useful articles