linkedin Skip to Main Content
Just announced! We now support spreadsheets
Back to blog

CSS Pseudo-Classes: The Must-Have, the Unusual, and the Experimental


Pseudo-classes are CSS features that allow us to do pretty amazing things when it comes to stylizing our content.

Some common uses are applying styles to:

  • Mouse/keyboard interactions (ex: :hover, :active, :focus), 
  • Fields based on their content (ex: :blank, :valid, :checked) or 
  • Elements based on their position in the DOM (ex: :first-child, :nth-child(), :root).

Pseudo-classes themselves are keywords that we add after our selector.

🗒️ That syntax might remind you of pseudo-elements like ::before and ::selection. Pseudo-classes and pseudo-elements are different things. Pseudo-classes target states of an element like hover, disabled, or visited. Pseudo-elements affect elements that aren’t explicitly written to the DOM, like a before element, a backdrop, or the selection. This post will focus on pseudo-classes only.

In this post, we’ll deep dive into pseudo-classes: the must-have, the unusual, and even the brand-new experimental ones!

Relative Selectors

Relative selectors are any pseudo-class used to select an element based on its position on the DOM.

First, we have  :first-child and :last-child. As their name suggests, they are used to select an element’s first or last child.

On top of that, you can use :nth-child whenever you need to select the nth element.

Here is an example:

/* Make the first li red */

/* Make the 3rd li blue */
  color : blue
}Code language: CSS (css)

Another example of a relative selector I use all the time for styling tables is the :nth-child(even) selector that selects even elements.

There is also the standard trick of setting a margin-bottom to some elements and making it 0px on the :last-child.

💡 For more information, read the MDN documentation for relative selectors, referred to as Tree structural pseudo-classes.

User Interaction Selector

A user interaction selector is any selector responding to user input like a mouse click, a hover, or an element focused on with the keyboard.

In CSS development, we use these a lot, so let’s dive into them!

Link status pseudo-classes

First, let’s talk about the links. You can use the :visited pseudo-class to target any link that a user visited:

  color: #c58af9;
}Code language: CSS (css)

That is the purple color you see on the Google results you’ve already visited:

Results from Google. The link you already visited appears in purple instead of blue, stylized using the :visited pseudo-class.

You can also target non-visited links by using the :link pseudo-class.

In great news for CSS developers, another fascinating pseudo-class has recently been released — :any-link. It combines :visited and :link, and it targets all links (and area) with a valid href attribute.

There’s also :local-link, the recently developed but not-yet-supported pseudo-class that targets links of the same domain you’re currently on.

With these pseudo-classes, you can have a different style for a link pointing to an external domain than for the link pointing to the current website. If the href target is the same as the document URL, it will match.

Hover, Focus, and Active

To target an element hovered over by the mouse, use the :hover pseudo-class. We use this for links, buttons, and other elements that will be clickable.

Here is how you make a link go red on hover:

  color: red;
}Code language: CSS (css)

⚠️ In reality, you can use :hover on any element you want, but be careful. If not done sparingly, it will confuse your users and create a bad experience for them.

:focus is often used to target a field the user clicked or tabbed into. It’s super useful for form fields and buttons.

By default, the browser uses an outline on :focus, and of course you can overwrite this. That is how you edit the default outline:  

  outline: 2px dashed blue;
}Code language: CSS (css)

⚠️ Before you remove an outline on focus, remember that the browser uses an outline for accessibility concerns: keyboard users should be able to clearly identify what’s being focused on (aka what will be triggered when pressing Enter). More information on this matter can be found in the focus indicator section of Introduction to Web Accessibility by Corbin Crutchley.

:active is used when a click is being made or held. I always use this to make the button “pop” on a click. It provides excellent visual feedback to the user, so they know the website acknowledged their click.

I usually make a small transform: translateY(2px); when :active for my buttons — but you can get more creative, like the way Material UI Buttons react on clicks:

Focus-visible and Focus-within

By default, when the user clicks on something with their mouse, it activates the :focus styling, even if you want to show that styling only for the keyboard. We may not want that behavior when focusing with the mouse simply because it does not look good, but we need it when using the keyboard for accessibility reasons.

With the help of :focus-visible, you can style an element only when the user is focused with their keyboard. It will target elements that need to show focus, so it will target our element only if :focus is using tab.

So when focusing on an element, the browser would determine if it matched :focus-visible, based on the input method used.

💡 Check out the example below and the MDN documentation for more information on :focus-visible.

Now let’s talk about :focus-within. This powerful selector allows us to select an element based on its children’s focus state. Focus-within enables you to target a form that has a focus element within.

Let’s say you want the focus of an input to make the field container look focused instead of the input itself. You can disable the focus on the field and use :focus-within on the field container to make it look how you want it to.

Take a look at the example below that demonstrates both Focus-visible and Focus-within. If you focus on the email field, you’ll notice that the error message appears. Also, the focus is on the whole field, not just the input.

Try clicking the different buttons. You’ll see that the ones using focus-visible don’t show any outline:

The :target selector

Did you know that instead of carrying you to another page, a link can target an element of the current page instead?

If you use the # symbol and the element ID as an href attribute for your link, clicking it will make the user “jump” to the new element. In HTML, we call that an anchor. 

Anchors were always powerful and became even more so since the introduction of smooth-scrolling and scroll-padding-top. You can now make with native CSS what would have been quite a pain to do in JavaScript a few years ago.

The :target pseudo-class expands what you can do with anchors by letting us stylize the targeted element.

For our example below, you have a few paragraphs and a summary. By using :target, you can add an outline to the current paragraph:

💡 If you wonder how deep you can go with anchor links and the target pseudo-class, look at Kevin Powell recreating the Netflix carousel with only CSS.

Styling Form Fields

Many pseudo-classes are available for us to make our form fields look significantly better.

In this section, you’ll look at disabled, required, optional, valid, and invalid pseudo-classes.


A disabled element cannot be interacted with — usually, this will be an input, a select, or a button. To disable an element, use the disabled attribute on our element in HTML.

Visually, we show it by graying it out, making it evident that the element cannot be interacted with.

Required & Optional

In HTML, use the required attribute on an input to indicate its need to be filled before submitting the form. You can then use the :required pseudo-class to target those elements and put an emphasis on them.

CSS also provides the :optional pseudo-class to target the inputs that are not required.

Valid & Invalid

When dealing with fields in HTML, you can specify what types of data are acceptable and what types are not. Using a REGEX expression, you can ask for numbers, an email address, or even a custom pattern.

In CSS, inputs have a valid or invalid state that you can stylize using the :valid and :invalid selectors.

Usually, we display a red outline on invalid fields to clearly show the user which fields are blocking their form submission.


The example below is a form using all the form-related pseudo-classes we just mentioned.

  • The name field is required and is currently empty, so it’s invalid and has a red outline.
  • The address is not required, so it is valid and has a green border even though it’s empty.
  • The company is disabled, so its opacity is lower, and the cursor is set to not-allowed.
  • The email field is invalid as the value contains two @ symbols. It is invalid and is stylized with a red border.

And as you can see, the form itself can be stylized with :valid and :invalid too. A form needs all its fields to be valid to be valid itself:

New Pseudo-Classes

All the pseudo-classes mentioned in this post, except for :focus-within and :focus-visible, were available in the ecosystem for quite some time.

But in this section, we’ll talk about new and exciting stuff.

The :not selector

The :not pseudo-class is pretty much self-explanatory. It allows us to declare what we do NOT want in our selector. It makes life much easier sometimes to exclude elements from our selector instead of including all the ones we want.

The :not pseudo-class allows us to write a cleaner and more descriptive selector.

Let’s say you want to target every link that is not a button. You can now write it in a much simpler way:

/* without using :not */
a {
  color: brown;
.button {
  color: white;

/* using :not */
a:not(.button) {
  color: brown;
}Code language: CSS (css)

It doesn’t look like a lot, but when using the :not selector, you don’t even need to know the button’s color.

💡 Modern browsers have supported :not since 2020, look at the caniuse page for more information.

The :where & :is pseudo-classes

The :is selector allows us to write DRYer code by consolidating multiple selectors. Let’s say you need to target buttons from .header, .footer and .sidebar.

Without the :is selector, you might write something like this:

.header .button,
.sidebar .button {
  background: teal;
}Code language: CSS (css)

This feels like unnecessary repetition, doesn’t it?

Using :is you can write:

:is(.header, .foooter, .sidebar) .button {
  background: teal;
}Code language: CSS (css)

This code is shorter, easier to read, and supported in major web browsers.

To understand :where, we need to talk about selector specificity.

First, if you are unfamiliar with specificity, read this great deep dive from Smashing Magazine.

Also, to try your selector specificity, I recommend this great tool from Polypane.

When working with large CSS codebases, specificity is always a struggle. When defining defaults, we need the specificity to be as low as possible.

The :where pseudo-class acts like :is, but it always has a 0 specificity, when :is specificity will be the most specific in its selector list.

It makes the :where super helpful in writing base styles that can easily be overwritten.

💡 I recommend this post by Mojtaba Seyedi going through an actual use case of the where pseudo-class in the context of a CSS reset.

All about :has, a promising but experimental selector

The :has selector allows you to select an element based on its children. You can, for example, use it for selecting any field container with an :invalid input inside.

Unfortunately, as of this writing, the :has pseudo-class is not yet supported in major browsers.

The `:has` pseudo-class on caniuse. Currently only supported on Safari and the latest version of Chrome.

Still, we are getting there, and this is exciting progress for CSS developers.

💡 If you want to read more about :has, I recommend Ibadehin Mojeed’s article on it, How and when to use the CSS :has selector. Also, check the MDN documentation for it.

I’m Tom Quinonero, I write about design systems and CSS. Follow me on Twitter for more tips and resources 🤙

Sources and Links: