Lesson 17: Pseudo-classes
Say you’ve got an email sign up form, and you want the email field to have a red border if it holds an invalid email address.
How do you do that?
You can use the :invalid CSS pseudo-class, which is one of many pseudo-classes the browser gives you.
A pseudo-class lets you apply styles based on state changes and things outside your control. This means your design can react to user input, such as an invalid email address. These were covered in the selectors module, and this module will take you through them in more detail.
Pseudo-elements, which you can learn more about in the previous module, let you style parts of an element. Pseudo-classes are different. They hook onto a specific state that an element might be in.
Interactive states
The following pseudo-classes apply because of an interaction a user has with your page.
:hover
If a user has a pointing device like a mouse or trackpad, and they place it over an element, you can hook onto that state with :hover to apply styles.
This is a useful way to hint that an element can be interacted with.
:active
This state is triggered when an element is being interacted with, such as a click, before the click is released. If a pointing device like a mouse is used, this state is when the click starts and hasn’t yet been released.
:focus, :focus-within, and :focus-visible
If an element can receive focus, like a <button>, you can react to that state with the :focus pseudo-class.
You can also react if a child of your element receives focus with :focus-within.
Focusable elements, like buttons, show a focus ring when they are in focus, even when clicked. In this sort of situation, a developer will often apply the following CSS.
button:focus {
outline: none;
}This CSS removes the default browser focus ring when an element receives focus.
That creates an accessibility problem for users who navigate a web page with a keyboard.
If there is no focus style, they won’t be able to keep track of where focus is when using the tab key.
With :focus-visible you can show a focus style when an element receives focus from the keyboard.
You can still use the outline: none rule to hide it when a pointer device interacts with the element.
button:focus {
outline: none;
}
button:focus-visible {
outline: 1px solid black;
}:target
The :target pseudo-class selects an element that has an id matching a URL fragment.
Say you’ve got the following HTML.
<article id="content">
<!-- ... -->
</article>You can attach styles to that element when the URL contains #content.
#content:target {
background: yellow;
}This is useful for highlighting areas that might have been linked to directly, such as the main content on a website, using a skip link.
Historic states
:link
The :link pseudo-class can be applied to any <a> element that has a href value that hasn’t been visited yet.
:visited
You can style a link that the user has already visited with the :visited pseudo-class.
This is the opposite state to :link, but you have fewer CSS properties to use for security reasons.
You can only style color, background-color, border-color, outline-color, and the color of SVG fill and stroke.
Order matters
If you define a :visited style, it can be overridden by a link pseudo-class with at least equal specificity.
Because of this, it’s recommended that you use the LVHA rule for styling links with pseudo-classes in a particular order.
The order is :link, :visited, :hover, :active.
a:link {}
a:visited {}
a:hover {}
a:active {}[!NOTE] For security reasons, you can only change styles defined by a
:linkor unvisited state with the:visitedpseudo-class. So making sure you define changeable styles first is important. Sticking to the LVHA rule will help with that.
Form states
The following pseudo-classes can select form elements in the various states they might be in while you interact with them.
:disabled and :enabled
If a form element, such as a <button>, is disabled by the browser, you can hook onto that state with the :disabled pseudo-class.
The :enabled pseudo-class is available for the opposite state.
Form elements are also :enabled by default, so you might not find yourself reaching for this pseudo-class.
:checked and :indeterminate
The :checked pseudo-class is available when a supporting form element, such as a checkbox or radio button, is in a checked state.
The :checked state is a binary state, true or false.
But checkboxes do have an in-between state when they are neither checked nor unchecked.
This is known as the :indeterminate state.
An example of this state is when you’ve got a “select all” control that checks all checkboxes in a group. If the user then clears one of these checkboxes, the root checkbox no longer represents “all” being checked. So it should be put into an indeterminate state.
The <progress> element also has an indeterminate state that you can style.
A common use case is to give it a striped look to show that it’s unknown how much more is needed.
:placeholder-shown
If a form field has a placeholder attribute and no value, the :placeholder-shown pseudo-class can be used to attach styles to that state.
As soon as there is content in the field, whether it has a placeholder or not, this state no longer applies.
Validation states
You can respond to HTML form validation with pseudo-classes such as :valid, :invalid, and :in-range.
The :valid and :invalid pseudo-classes are useful for cases like an email field that has a pattern it needs to match to be a valid field.
This valid state can be shown to the user, helping them understand they can safely move on to the next field.
The :in-range pseudo-class is available if an input has a min and max, such as a numeric input, and the value is within those bounds.
With HTML forms, you can mark a field as required with the required attribute.
The :required pseudo-class is then available for required fields.
Fields that are not required can be selected with the :optional pseudo-class.
[!NOTE] It’s not a good idea to rely only on color to signal state changes, especially red and green. Colorblind and low-vision users can struggle to see a state change, or even miss it completely. A good idea is to use color to support state changes, along with text changes and icon changes to show the change visually.
Selecting elements by their index, order, and occurrence
There is a group of pseudo-classes that select items based on where they are in the document.
:first-child and :last-child
If you want to find the first or last item, you can use :first-child and :last-child.
These pseudo-classes return either the first or last element in a group of sibling elements.
:only-child
You can also select elements that have no siblings, with the :only-child pseudo-class.
:first-of-type and :last-of-type
You can select the :first-of-type and :last-of-type.
At first these look like they do the same thing as :first-child and :last-child, but consider this HTML.
<div class="my-parent">
<p>A paragraph</p>
<div>A div</div>
<div>Another div</div>
</div>And this CSS.
.my-parent div:first-child {
color: red;
}No elements would be colored red, because the first child is a paragraph and not a div.
The :first-of-type pseudo-class is useful here.
.my-parent div:first-of-type {
color: red;
}Even though the first <div> is the second child, it’s still the first of its type inside the .my-parent element.
So with this rule, it will be colored red.
:nth-child() and :nth-of-type()
You’re not limited to first and last children and types either.
The :nth-child() and :nth-of-type() pseudo-classes let you pick an element at a certain index.
Indexing in CSS selectors starts at 1.
The :nth-last-child() and :nth-last-of-type() pseudo-classes count from the end, rather than the beginning.
You can pass more than an index into these pseudo-classes too.
If you wanted to select all even elements, you can use :nth-child(even).
You can also create more complex selectors that find items at regular intervals, using the An+B microsyntax.
li:nth-child(3n+3) {
background: yellow;
}This selector picks every third item, starting at item 3.
The n in this expression is the index, which starts at zero.
The 3 in 3n is how much you multiply that index by.
Say you’ve got 7 <li> items.
The first item that is selected is 3, because 3n+3 works out to (3 * 0) + 3.
The next round picks item 6, because n has now gone up to 1, so (3 * 1) + 3.
This expression works for both :nth-child() and :nth-of-type().
:nth-child() and :nth-last-child() also support an “of S” syntax that lets you filter the matches with a selector, similar to :nth-of-type().
li:nth-of-type(even) is the same as :nth-child(even of li).
While :nth-of-type() only lets you filter based on element type, like li or p, the “of S” syntax lets you filter on any selector.
If you’ve got a table, you may want to add stripes to every other row.
You can target every other row with tr:nth-child(even), but this doesn’t work if you are filtering some rows out.
If you handle filtering by adding the hidden attribute, you can add of :not([hidden]) to the selector to filter the hidden items out before selecting the even rows.
tr:nth-child(even of :not([hidden])){
background: lightgrey;
}You can play around with this sort of selector on this nth-child tester or this quantity selector tool.
:only-of-type
Lastly, you can find the only element of a certain type in a group of siblings with :only-of-type.
This is useful if you want to select lists with only one item, or if you want to find the only bold element in a paragraph.
Finding empty elements
It can sometimes be useful to find completely empty elements, and there is a pseudo-class for that too.
:empty
If an element has no children, the :empty pseudo-class applies to it.
Children aren’t just HTML elements or text nodes though.
They can also be whitespace, which can be confusing when you’re debugging the following HTML and wondering why it isn’t working with :empty.
<div>
</div>The reason is that there’s some whitespace between the opening and closing <div>, so :empty won’t match.
The :empty pseudo-class can be useful if you’ve got little control over the HTML and want to hide empty elements, such as in a WYSIWYG content editor.
Here, an editor has added a stray, empty paragraph.
<article class="post">
<p>Donec ullamcorper nulla non metus auctor fringilla.</p>
<p></p>
<p>Curabitur blandit tempus porttitor.</p>
</article>With :empty, you can find that and hide it.
.post :empty {
display: none;
}Finding and excluding multiple elements
Some pseudo-classes help you to write more compact CSS.
:is()
If you want to find all of the h2, li, and img child elements in a .post element, you might think to write a selector list like this.
.post h2,
.post li,
.post img {
...
}With the :is() pseudo-class, you can write a more compact version.
.post :is(h2, li, img) {
/* ... */
}The :is pseudo-class is not only more compact than a selector list, it’s also more forgiving.
In most cases, if there’s an error or unsupported selector in a selector list, the whole selector list stops working.
If there’s an error in the selectors you pass to an :is pseudo-class, it ignores the invalid selector but uses the ones that are valid.
:not()
You can also exclude items with the :not() pseudo-class.
For example, you can use it to style all links that don’t have a class attribute.
a:not([class]) {
color: blue;
}A :not pseudo-class can also help you improve accessibility.
For example, an <img> must have an alt, even if it’s an empty value.
So you could write a CSS rule that adds a thick red outline to invalid images.
img:not([alt]) {
outline: 10px red;
}:has()
What if you want to style elements based on what is inside them?
You can use the :has() pseudo-class to do that.
For example, you might want to apply styles to buttons that include icons.
button:has(svg) {
/* ... */
}In its most basic form, like in the previous example, you can think of :has() as a parent selector.
You can also combine the matching parent selector with other selectors to target other elements.
form:has(input:valid) label {
font-weight: bold;
}
form:has(input:valid) label::after {
content: "✅";
}In this example we are applying styles to the label element and the label::after pseudo-element when the form input has a valid pseudo-class.
The :has() pseudo-class can’t be nested inside of another :has(), but it can be combined with other pseudo-classes.
:is(h1, h2, h3):has(a) {
/* ... */
}The selector list is unforgiving, so if any selectors in the list are invalid, all the style rules are ignored.
.my-element:has(img, ::before) {
/* any styles here will be discarded since pseudo elements can't be included in the :has() selector list */
}Adapted from Learn CSS © Google and contributors, licensed under CC BY 4.0 (prose) and Apache 2.0 (code samples).