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.
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;
}
:hoverapplies while the mouse pointer is over the element.:activeapplies while the element is being pressed (mouse down).:focusapplies when the element has keyboard focus, for example after you tab to a link or click into a text field.:focus-visibleis like:focusbut 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.
Link states
Links have their own states:
:linkis a link the visitor has not opened yet.:visitedis 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;
}
:disabledand:enabledfor controls that are turned off or on.:checkedfor ticked checkboxes and selected radio buttons.:required,:optional,:validand:invalidfor validation styling.:placeholder-shownwhile 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-childand:last-childpick 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 isan + b, for example2n + 1is the same asodd.:nth-last-child(1)counts from the end.:first-of-typeand: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 awidth,heightandbackground, usually withdisplay: 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
inlineby 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.
::beforeand:hoverare the modern forms. - Forgetting
contenton::beforeand::after. Without it, nothing is created. - Putting a space before the colon:
.btn :hovermeans “a hovered element inside.btn”. Write.btn:hover. - Putting
:hoverrules before the normal rule with the same strength. The later rule wins, so put the base style first. - Wrong link order (
:hoverbefore: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
- Click Practice in Editor. Make the menu links change colour on
:hoverand on:focus-visible. Press Tab to test the keyboard. - Style the second button (
disabled) with:disabledso it looks unclickable. - Give the email input a blue ring on
:focus. Then tick the checkbox and make its text green and bold. - Zebra-stripe the to-do list with
:nth-child(even), then bold every third item with:nth-child(3n). - Add a star after every required label and an arrow after every external link, using only CSS.
- Turn the first letter of
.storyinto a drop cap and set the::selectioncolour of the whole page. - 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. ::beforeand::afteralways need acontentproperty, 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.