CSSBeginner

CSS Pseudo-classes and Pseudo-elements: :hover, :focus, ::before

Style hover, focus, checked and disabled states, pick items with :nth-child, and add decorative content with ::before and ::after, step by step.

All CSS lessons

What you will learn

So far your selectors have pointed at things that are always there: tags, classes and ids. But web pages are alive. A button is hovered, a field gets focus, a checkbox is ticked, the third row of a list needs a stripe. Pseudo-classes let CSS react to these states and positions. Pseudo-elements let you style parts of an element, or add decorative content with no extra HTML. In this lesson you will learn the most useful ones and see exactly which element each one lands on.

The HTML we will style

<h1 class="page-title">Interactive Lab</h1>

<nav class="menu">
  <a href="#home" class="menu-link">Home</a>
  <a href="#about" class="menu-link">About</a>
  <a href="https://example.com" class="menu-link external">Partner</a>
</nav>

<p>
  <button class="btn">Hover me</button>
  <button class="btn" disabled>Disabled</button>
</p>

<form class="signup">
  <label class="field-label required" for="email">Email</label>
  <input class="field" id="email" type="email" placeholder="you@example.com">

  <label class="check">
    <input type="checkbox" id="agree">
    <span class="check-text">I agree to the terms</span>
  </label>
</form>

<ul class="todo">
  <li>Learn HTML</li>
  <li>Learn CSS</li>
  <li>Learn JavaScript</li>
  <li>Build a project</li>
  <li>Get a job</li>
</ul>

<p class="story">CSS makes the web beautiful. Every great website you have seen uses it to look good on every screen.</p>

<p>Price: <span class="price">499</span></p>
Selector Lands on When
.menu-link:hover any of the three <a class="menu-link"> links while the mouse is over it
.btn:hover, .btn:active the two <button class="btn"> hovered, and while pressed
.btn:disabled the second button it has the disabled attribute
.field:focus <input class="field"> when the cursor is in it
.check input:checked + .check-text the <span class="check-text"> after the checkbox is ticked
.todo li:nth-child(odd) the 1st, 3rd and 5th <li> always
.field-label.required::after the <label> adds a star after its text
.price::before the <span class="price"> adds a currency sign before 499
.story::first-letter the <p class="story"> styles its first letter

Pseudo-classes: styling a state (one colon)

A pseudo-class is written with one colon after a selector: selector:state. The style applies only while the element is in that state.

Mouse and keyboard states

/* the three <a class="menu-link"> links */
.menu-link {
  display: inline-block;
  padding: 8px 14px;
  color: royalblue;
  text-decoration: none;
}

.menu-link:hover {
  color: white;
  background-color: royalblue;
}
  • :hover applies while the mouse pointer is over the element.
  • :active applies while the element is being pressed (mouse down).
  • :focus applies when the element has keyboard focus, for example after you tab to a link or click into a text field.
  • :focus-visible is like :focus but only when the browser thinks a visible focus ring is helpful (mostly keyboard users). It is the best choice for buttons and links.
/* the two <button class="btn"> */
.btn {
  padding: 10px 20px;
  border: none;
  background-color: seagreen;
  color: white;
  cursor: pointer;
}

.btn:hover {
  background-color: darkgreen;
}

.btn:active {
  background-color: black;
}

.btn:focus-visible {
  outline: 3px solid orange;
  outline-offset: 2px;
}

Never rely on hover alone. Touch screens have no hover, and keyboard users never trigger it. Anything important (like a menu) must also work with focus or a tap. And do not remove the focus outline (outline: none) without giving a clear replacement.

Links have their own states:

  • :link is a link the visitor has not opened yet.
  • :visited is a link they have opened.

If you style all four, write them in this order, often remembered as LoVe HAte:

a:link    { color: royalblue; }
a:visited { color: purple; }
a:hover   { color: crimson; }
a:active  { color: black; }

The order matters, because later rules override earlier ones when the states overlap (for example, a link that is both visited and hovered).

Form states

Forms have plenty of states you can target:

/* the <input class="field"> */
.field {
  padding: 8px 10px;
  border: 2px solid #cccccc;
}

.field:focus {
  border-color: royalblue;
  outline: none;
  box-shadow: 0 0 0 3px rgba(65, 105, 225, 0.3);
}

/* the disabled button */
.btn:disabled {
  background-color: #cccccc;
  cursor: not-allowed;
}
  • :disabled and :enabled for controls that are turned off or on.
  • :checked for ticked checkboxes and selected radio buttons.
  • :required, :optional, :valid and :invalid for validation styling.
  • :placeholder-shown while a field is still empty.

Notice the .field:focus rule replaced the default outline with a box-shadow ring. That is fine, because there is still a clear visible focus indicator.

The checkbox trick

:checked combined with the sibling selector + (from the previous lesson) lets you react to a checkbox without JavaScript:

/* the <span class="check-text"> right after the checked checkbox */
.check input:checked + .check-text {
  color: seagreen;
  font-weight: bold;
}

Read it right to left: “the .check-text that comes immediately after an input that is :checked, inside .check”. Tick the box in the preview and the text turns green and bold.

Position states: pick items by their place

These pick an element based on where it sits among its siblings:

/* the <li> elements of .todo */
.todo li:first-child {
  font-weight: bold;
}

.todo li:last-child {
  color: seagreen;
}

.todo li:nth-child(odd) {
  background-color: #eef2ff;
}
  • :first-child and :last-child pick the first and last sibling.
  • :nth-child(n) picks the nth one, counting from 1.
  • :nth-child(odd) and :nth-child(even) give you zebra stripes.
  • :nth-child(3n) picks every third (3rd, 6th, 9th…). The general form is an + b, for example 2n + 1 is the same as odd.
  • :nth-last-child(1) counts from the end.
  • :first-of-type and :nth-of-type() count only among siblings of the same tag, ignoring others.

:not()

:not(selector) matches everything that does not match what is inside the brackets:

/* every <li> except the last one gets a line under it */
.todo li:not(:last-child) {
  border-bottom: 1px solid #dddddd;
}

This is a neat way to put separators between items without a stray line at the end.

Pseudo-elements: styling a part (two colons)

A pseudo-element is written with two colons (::) and targets a part of an element, or an extra piece that does not exist in the HTML.

::before and ::after

These insert a new piece of content just inside the start or end of an element. They must have a content property, or nothing appears.

/* a star after the label text (the <label class="field-label required">) */
.required::after {
  content: " *";
  color: crimson;
}

/* a currency sign in front of the <span class="price"> */
.price::before {
  content: "₹";
  font-weight: bold;
}

/* an arrow after external links */
.external::after {
  content: " ↗";
}

The HTML stays clean (<span class="price">499</span>) and the decoration lives in the CSS. Some points to remember:

  • content: "" (an empty string) is valid. It is used to create decorative shapes: you then give the pseudo-element a width, height and background, usually with display: block.
  • The inserted content is not part of the page’s text. It cannot be selected, and it may not be reliably read by screen readers. Use it only for decoration, never for important information.
  • The pseudo-element is inline by default.

Styling part of the text

/* the first letter of <p class="story"> as a drop cap */
.story::first-letter {
  font-size: 3rem;
  font-weight: bold;
  color: royalblue;
  float: left;
  margin-right: 8px;
  line-height: 1;
}

/* the first line of <p class="story"> */
.story::first-line {
  font-variant: small-caps;
}

Other useful pseudo-elements:

/* text that the visitor has highlighted with the mouse */
::selection {
  background-color: gold;
  color: black;
}

/* the placeholder text inside inputs */
.field::placeholder {
  color: #999999;
  font-style: italic;
}

One colon or two?

Pseudo-classes use one colon (:hover), pseudo-elements use two (::before). A simple way to remember: a class is a state of an element, an element is a piece of it. Older code writes :before with one colon, and browsers still accept that.

Putting it together

.menu-link {
  display: inline-block;
  padding: 8px 14px;
  color: royalblue;
  text-decoration: none;
}

.menu-link:hover,
.menu-link:focus-visible {
  color: white;
  background-color: royalblue;
}

.external::after {
  content: " ↗";
}

.btn {
  padding: 10px 20px;
  border: none;
  background-color: seagreen;
  color: white;
  cursor: pointer;
}

.btn:hover {
  background-color: darkgreen;
}

.btn:disabled {
  background-color: #cccccc;
  cursor: not-allowed;
}

.field {
  padding: 8px 10px;
  border: 2px solid #cccccc;
}

.field:focus {
  border-color: royalblue;
  outline: none;
  box-shadow: 0 0 0 3px rgba(65, 105, 225, 0.3);
}

.required::after {
  content: " *";
  color: crimson;
}

.check input:checked + .check-text {
  color: seagreen;
  font-weight: bold;
}

.todo li:nth-child(odd) {
  background-color: #eef2ff;
}

.todo li:not(:last-child) {
  border-bottom: 1px solid #dddddd;
}

.price::before {
  content: "₹";
}

.story::first-letter {
  font-size: 3rem;
  color: royalblue;
  float: left;
  margin-right: 8px;
  line-height: 1;
}

Common mistakes

  • Using one colon for a pseudo-element in new code, or two for a pseudo-class. ::before and :hover are the modern forms.
  • Forgetting content on ::before and ::after. Without it, nothing is created.
  • Putting a space before the colon: .btn :hover means “a hovered element inside .btn”. Write .btn:hover.
  • Putting :hover rules before the normal rule with the same strength. The later rule wins, so put the base style first.
  • Wrong link order (:hover before :link), which makes some states never appear.
  • Removing focus outlines without adding another clear focus style.
  • Counting from 0 in :nth-child. It starts at 1.
  • Putting important text in content. It is decoration only.
  • Depending on hover for things touch users must reach.

Practice

  1. Click Practice in Editor. Make the menu links change colour on :hover and on :focus-visible. Press Tab to test the keyboard.
  2. Style the second button (disabled) with :disabled so it looks unclickable.
  3. Give the email input a blue ring on :focus. Then tick the checkbox and make its text green and bold.
  4. Zebra-stripe the to-do list with :nth-child(even), then bold every third item with :nth-child(3n).
  5. Add a star after every required label and an arrow after every external link, using only CSS.
  6. Turn the first letter of .story into a drop cap and set the ::selection colour of the whole page.
  7. Draw a line between list items without a line after the last item, using :not(:last-child).

Recap

  • Pseudo-classes (one colon) style an element in a certain state or position: :hover, :focus, :focus-visible, :active, :disabled, :checked, :first-child, :nth-child(), :not().
  • Keep link states in the order Link, Visited, Hover, Active.
  • Pseudo-elements (two colons) style a part of an element or add decoration: ::before, ::after, ::first-letter, ::first-line, ::selection, ::placeholder.
  • ::before and ::after always need a content property, and are for decoration only.
  • Do not depend on hover, and never remove the focus indicator without a replacement.
  • Next you will learn the rules that decide which style wins when several selectors match.