Guide to Advanced CSS Selectors - Part Two

Continuing from part one, this episode will focus on the advanced CSS selectors categorized as pseudo classes and pseudo elements and practical applications for each. We'll especially try to make sense of the syntax for nth-child.

Part Two (this article):

Part One:

Pseudo Classes

This is the largest category, and also the most context-dependent.

Pseudo classes are keywords that are applied when they match the selected state or context of an element.

These vastly increase the capabilities of CSS and enable functionality that in the past was often erroneously relegated to JavaScript.

Some selectors are stateful:

  • :focus
  • :hover
  • :visited
  • :target
  • :checked

While others attach to the order of elements:

  • :nth-child() / :nth-of-type()
  • :first-child / :first-of-type
  • :last-child / :last-of-type
  • :only-child / :only-of-type

Then there's the highly useful pseudo class :not(), the newly supported :is(), as well as the :root pseudo class that has come to the forefront as CSS custom properties (variables) have risen in support.

Review the MDN docs for the full list of available pseudo classes including available options specific to form inputs, video captions, and language, as well as some currently in progress towards implementation.

Practical Applications For Pseudo Classes

Zebra Striped Table Rows

The nth series of selectors has endless applications. Use them for anything you want to occur in any sort of repetitive pattern using a 1-based index.

An excellent candidate for this is zebra striping of table rows.

The nth-child selector can use an integer, be defined using functional notation, or the keywords of even or odd. We'll use the keywords to most efficiently produce our zebra striping rule:

tbody tr:nth-child(odd) {
  background-color: #ddd;
}

Which will produce the following:

a two-column table where rows in the table body have a light grey background for every other (odd) table row background

Apply Alternating Background Colors

Using the functional notation for nth-child we can alternate through a series of background colors and ensure the pattern repeats in the defined order no matter how many elements exist. So a pattern of rebeccapurple, darkcyan, lightskyblue will repeat in that order.

The way this works is to define the total number of colors - 3 - alongside n, which represents all positive numbers starting from 0, and which will be multiplied by the associated number, in this case, 3. So by itself, 3n would select the 3rd item, the 6th item, the 9th item, and so on. It would not select the first in the list because 3 x 0 = 0.

For our repetitive pattern, we want the first item selected to be the first color in our palette.

So, we extend the notation to be 3n + (integer corresponding to color order), therefore our first color rule becomes:

li:nth-child(3n + 1) {
  background-color: rebeccapurple;
}

And this selects every third element, starting with the first one:

5 stacked items where the first and fourth have the background-color: rebeccapurple applied

Essentially, that + [number] shifts the starting index.

To complete our pattern we define the following rules, incrementing the added number to be the order of the color in the repeating pattern:

li:nth-child(3n + 2) {
  background-color: darkcyan;
}

li:nth-child(3n + 3) {
  background-color: lightskyblue;
}

Producing the following completed result:

By Stephanie Eckles (@5t3ph)

For an extended guide to nth-child checkout the recipe reference from CSS-Tricks and the nth-child tester to explore constructing these selectors.

Enjoying this guide and finding some useful solutions? I'd appreciate a coffee to help keep me motivated to create more resources! I also offer front-end reviews and mentoring sessions, choose an option to support me.

Removing Extra Spacing From Child Elements

If you are not using a reset that starts all elements with zero margin, you may encounter typography elements creating extra unwanted margins that unbalance spacing within a visual container.

Pseudo classes do not always need to be directly attached to an element, meaning we can do the following rule which attaches to any element that happens to be the last child of any parent and ensures it has no margin-bottom:

:last-child {
  margin-bottom: 0;
}

Excluding Elements From Selectors

Careful application of :not() is very useful for excluding elements from being selected.

We explored a few uses of :not() within the attribute selector section, notably a:not([class]) for targeting links that have no other classes applied.

:not() is excellent for use in utility frameworks or design systems to increase specificity on classes that have the potential to be applied to anything and for which there are known issues on certain combinations.

An extended example of excluding it for classes with links is when you are adjusting contrast for text, possibly in a dark mode context, and want to apply the contrast adjustment to text links as well:

/* Non dark mode application */
a:not([class]) {
  color: blue;
}

/* Update text color for dark mode */
.dark-mode {
  color: #fff;
}

/* Extend the color update to links via `inherit` */
.dark-mode a:not([class]) {
  color: inherit;
}

You can also chain :not() selectors, so perhaps you want to create a rule for form field inputs, but not of certain types:

input:not([type="hidden"]):not([type="radio"]):not([type="checkbox"])

It's also possible to include other pseudo selectors within :not() such as to exclude the :disabled state for buttons:

button: not(: disabled);

This allows you to have tidier rules by defining a reset of button styles first, then only apply coloration styles, borders, etc to non-disabled buttons instead of removing those styles for button:disabled later.

Efficiently Select Groups of Elements

The newly supported :is() pseudo class:

"...takes a selector list as its argument, and selects any element that can be selected by one of the selectors in that list." - MDN docs on :is()

One way this can make a big impact is more compactly selecting typography elements such as:

:is(h1, h2, h3, h4) ;

Or to scope layout styles more succinctly, such as:

:is(header, main, footer) ;

We can even combine :is() with :not() and really trim down our selectors, in this case selecting elements that are not headlines:

:not(:is(h1,h2,h3,h4))

To see this selector in context, check out the Smol Composable Card Component from the ModernCSS companion project, SmolCSS.dev.

For the immediate future, you'll want to include at least the webkit prefix version if you want to start using this selector. Due to a quirk in how browsers use selectors, you'll want to make this a unique rule separate from is() to avoid the browser throwing both rules out.

:-webkit-any(header, main, footer) ;

Join my newsletter for article updates, CSS tips, and front-end resources!

Style the Current Anchor Linked Element

When an element is the target of an anchor link (document fragment identifier) - https://url.com/#anchor-here - you can style it using :target.

I rely on anchor links for my project 11ty.Rocks, such as can be seen visiting this link to the CSS Houdini Worklet Generator.

The :target pseudo class should be placed on the element that contains the id attribute. However, you can chain it with descendent selectors to affect nested elements - maybe you want to give article:target h2 a larger size or something like that.

Leveraging :target I add a little extra message by combining with the pseudo element ::before to help indicate to the visitor which item they were provided a link for, which appears as follows ("It's me you're looking for...")

the :target style as applied to an article on 11ty.Rocks which has the message as described prior to this image

Bonus tip: ensure a bit of spacing prior to the top of the element on scroll by using scroll-margin-top: 2em; (or another value of your choosing). This should be considered a progressive enhancement, be sure to review browser support for scroll-margin-top.

Visually Indicate Visited Archive Links

The :visited pseudo class is very unique because of the potential to be exploited in terms of user's privacy. To resolve this, browser makers have limited which CSS styles are allowed to be applied using :visited.

Una Kravets has a much more in-depth reference exploring how to create useful :visited styles, but here's the reduced version which I have in use for visitors of Style Stage to track which styles they have already viewed.

A key gotcha is that styles applied via :visited will always use the parent's alpha channel - meaning, you cannot use rgba to go from invisible to visible, you must change the whole color value.

So, to hide the initial state, you need to be able to use a solid color, such as the page background color.

Additionally, for accessibility, it may not be desirable for the pseudo element content to be read out if it's an icon or emoji since we cannot supply an accessible name for content values. There is inconsistencies between assistive technology in whether pseudo element content is read, so we can try to ensure it's ignored with the use of aria-hidden="true".

Our first step then is to add a span within links and that is what we will ultimately apply the :visited stylings to:

<a href="#">Article Title <span aria-hidden="true"></span></a>

The default styling (non-visited) adds the pseudo element, and makes the color the same as the page background to hide it visually:

a span[aria-hidden]::after {
  content: "โœ”";
  color: var(--color-background);
}

Then, when the link has been visited, we update the color to make it visible:

a:visited span[aria-hidden]::after {
  color: var(--color-primary);
}

Advanced Interactions With :focus-within

An up and coming pseudo class is :focus-within for which a polyfill is available, but otherwise it should be used with caution or as a progressive enhancement.

The :focus-within pseudo class as described by MDN docs:

The :focus-within CSS pseudo-class represents an element that has received focus or contains an element that has received focus. In other words, it represents an element that is itself matched by the :focus pseudo-class or has a descendant that is matched by :focus.

For a practical way to use :focus-within review the tutorial for a CSS-Only Accessible Dropdown Navigation Menu.

Pseudo Elements

Pseudo elements allow you to style a specific part of the selected element. They vary quite widely in application, with the (currently) best supported ones being the following:

  • ::after
  • ::before
  • ::first-letter
  • ::first-line
  • ::selection

Practical Applications For Pseudo Elements

Extra Visual Elements For Styling Benefits

The ::before and ::after pseudo elements create an additional element that visually appears to be part of the DOM, but is not part of the real HTML DOM. They are as fully style-able as any real DOM element.

I have used these elements for all sorts of embellishments. Since they act like real elements, they are computed as child elements when using flexbox or CSS grid layout, which has greatly increased their functionality in my toolbox.

A few key concepts for using ::before and ::after:

  • Requires the content property before being made visible, but this property can be set to a blank string - content: "";
  • Critical text content should not be included in the content value since it's inconsistently accessed by assistive technology
  • Unless positioned otherwise, ::before will display prior to the main element content, and ::after will display after it.

Here's a demo of the default behavior of these with just a little bit of styling applied:

A short paragraph showing the the text of "Before" in sitting prior to the paragraph content and the text of "After" sitting after the paragraph content

Notice they act like inline elements by default, and follow the wrapping behavior as well for longer content:

A slightly longer paragraph that has wrapping lines showing the the text of "Before" in sitting prior to the paragraph content and the text of "After" sitting after the paragraph content

And here's with the singular adjustment to add display: flex to the paragraph:

The same multiline paragraph but the "Before" appears as a column to the left of the paragraph content, and "After" appears as a column after the paragraph content

And here's with swapping that to display: grid:

The same multiline paragraph but the "Before" appears as a row on top of the paragraph content, and "After" appears as a row below the paragraph content

The ::before and ::after elements are quick ways to add simple, consistent typography flourishes, a few of which can be seen in this CodePen demo:

By Stephanie Eckles (@5t3ph)

Did you catch the trick in the emoji one?

We can retrieve the value of any attribute on the element to use in the content property via the attr() function:

/*
<h2 class="emoji" data-emoji="๐Ÿ˜">
*/
.emoji::before {
  content: attr(data-emoji);
}

Here's a gist of how to display element id and class values in pseudo elements using this same idea.

Emphasize an Article Lead

The "lede" (pronounced "lead") is a newsprint term for the first paragraph within a news article and is intended to be a summary of the key point of the article (you may have heard the phrase "Don't bury the lede!").

We can combine the pseudo class of :first-of-type with the pseudo element of :first-line to emphasize the first line of paragraph copy. Interestingly, this is dynamic and will change as the viewport size changes.

article p:first-of-type:first-line {
  font-weight: bold;
  font-size: 1.1em;
  color: darkcyan;
}

Producing the following inherently responsive result:

gif demo of resizing the viewport when the previously described rule is applied and seeing that as the number of words in the first line of the paragraph changes the style continues to only affect the very first line

Ensure Accessible Contrast For Text Selection

A frequently missed style is for text selection, despite it being an interaction many of us engage in multiple times a day.

While browsers try to handle styling this event, it is possible to lose contrast. I encountered this when styling ModernCSS.dev due to the darker theme in use.

To resolve, we can use the ::selection pseudo element to supply a custom text color and background color:

::selection {
  background: yellow;
  color: black;
}

Custom List Bullet Styles

An up and coming pseudo element specifically for styling list bullets is ::marker.

For the browser support link and an example of how to use it, check out the section in my tutorial on Totally Custom List Styles.

Additional Resources

  • MDN Docs on CSS selectors
  • Selectors Explained - enter any selector to learn what is being affected
  • CSS specificity calculator by Polypane - discover the level of specificity of a selector
  • CSS Diner - a game to test your ability to create CSS selectors
  • Style Stage - a great place to practice your new selector knowledge is my other project, Style Stage, which is a modern CSS showcase styled by community contributions. A limitation is the inability to add new classes or IDs, so you will need to exercise your selector abilities to successfully create your stylesheet submission!

Head back to part one to learn about five other categories of advanced CSS selectors. And, if you learned something from this guide and you're able, I'd appreciate a coffee to help me bring you more tutorials and resources!