· 6 min read

Useful Pseudo-classes for Layout

This article was auto-translated from Chinese. Some nuances may be lost in translation.

In recent years, CSS has introduced many pseudo-classes, and they are no longer implemented by only a few browsers like they used to be. Today, I’d like to introduce several CSS pseudo-classes and show practical examples to help with styling and layout.

  • :is
  • :where
  • :has
  • :lang
  • :focus-within
  • :first-child and :last-child

:is

Previously, when we wanted to apply the same CSS rules to multiple selectors, we would write:

.a-class,
.b-class {
  font-size: 25px;
}

There is nothing wrong with this approach, but with :is, we can simplify the declaration to:

:is(.a-class, .b-class) {
  font-size: 25px;
}

If you have many selectors to apply, using :is makes the code much easier to read. Although it is still a Working Draft, according to data from Can I use, it is supported for 98.1% of users.

In addition to individual classes, it can also be combined with other selectors:

:is(.a-class, .b-class) a:hover {
  opacity: 0.8;
}

:where

:where functions the same way as :is. However, there is a difference in specificity. :where has no specificity weight—its specificity is equivalent to *. In contrast, :is takes on the weight of the most specific selector in its list.

In this example, because of #page, the specificity is higher than the class selector, so the resulting color is red. If you use :where, the selectors inside do not add extra specificity.

Therefore, the final applied color will be yellow. Based on the results above, their use cases can be divided into:

  • Using :where() for global styles (such as CSS resets), making them easy to override later.
  • Using :is() for local styles across multiple selectors, ensuring all selectors share the same specificity.

:has

:has can match a parent element when specified conditions on its child elements are met. That explanation might sound a bit convoluted, so an example makes it much clearer. A very practical scenario is “changing the parent element’s style when a child element is focused.” Sometimes when implementing a search feature, we place a search icon next to the input field. When the input is focused, we want not just the input field to have focus styling, but the entire input wrapper including the icon to reflect that focus state. In this case, input:focus alone cannot achieve what we want. Past solutions involved either modifying the DOM structure so CSS selectors could work, or using JavaScript to listen for focus events and toggle a class on the parent element. With :has, we can achieve this directly in CSS without altering the DOM structure.

This way, when the input is focused, .form:has(input:focus) activates and applies the style to the parent element. Being able to “match a parent element when a child element meets a condition” has been a long-standing wish for front-end developers, and we have finally taken a big step forward. :has has other use cases as well, especially whenever you want to apply styles based on a condition met by a child element.

For example, when switching themes, using body:has(input[type="checkbox"]:checked) allows you to toggle color themes without JavaScript while maintaining a clean, sensible DOM structure.

For more use cases of :has, you can check out the post on the WebKit blog. :has is a relatively new pseudo-class, and browser support is not quite as high yet (82.92%), so you may want to have a fallback plan when adopting it.

:focus-within

More experienced front-end developers might have noticed that the previous example could actually be replaced by :focus-within. :focus-within matches when any descendant element is focused, allowing you to modify the parent element’s styles. For instance, if you want to add a box-shadow to the form when it is focused:

However, :focus-within only applies to focus states and cannot handle more diverse selection criteria. If you need to match conditions other than focus, you can turn to the more versatile :has.

:lang

When building multilingual websites, a lang attribute is typically added to <html> to indicate the current site language. Sometimes we need to display different fonts depending on the language. Without :lang, you might have to write:

html[lang="zh"] p { font-family: var(--font-zh); }
html[lang="en"] p { font-family: var(--font-en); }

This is somewhat verbose. Using :lang simplifies the selector syntax:

p:lang(en) {
  font-family: var(--font-en);
}

p:lang(zh) {
  font-family: var(--font-zh);
}

:first-child and :last-child

Although these two are very common pseudo-classes, I have noticed quite a few front-end developers who are still unfamiliar with them. These pseudo-classes target the first or last child element. The most common scenario is wanting to set margin-right on items without applying it to the last element:

.tab {
  margin-right: 8px;
}

.tab:last-child {
  margin-right: 0;
}

This can also be simplified by pairing it with :not:

.tag:not(:last-child) {
  margin-right: 8px;
}

Why Not Just Use JavaScript?

Some users may not have JavaScript enabled—especially on content-heavy static sites like blogs. When you want to maintain a good reading experience without relying on scripts, the pseudo-classes covered above can help solve these layout and styling challenges. Why make things complicated when they can be simple?

Related Posts

Explore Other Topics