Level Up Your CSS Skills With The :has() Selector

Using :has() gives us the ability to “look ahead” with CSS and style a parent or ancestor element. Then, we can broaden the selector to target one or more siblings or children. By considering element states or positions, we can style nearly any combination of elements as unique singles or ranges.

Note: At present, support for :has() is rising, with it being available as of Safari 15.4 and Chrome/Edge 105. It is also behind a flag in Firefox as of version 103. Until full support is available, check out this tip for supporting :has today from Bramus Van Damme.

How :has() Works With Combinators And Pseudo-Classes

To best understand how the advanced selectors we’ll be creating work, we’ll quickly review the most relevant combinators and pseudo-classes.

A “combinator” is a special character that denotes the type of relationship between selector parts. Here are the core combinators to know:

  • space character: the descendent combinator matches a direct or nested child;
  • >: the direct child combinator matches only top-level, un-nested children;
  • +: the adjacent sibling combinator matches only the very next sibling;
  • ~: the general sibling combinator matches one or more siblings following the base selector.

The first stage of creating complex selectors is to append a pseudo-class to one or more parts. A “pseudo-class” defines a special state of an element, like :hover, and has the format of a single colon followed by the name. The :has() pseudo-class is considered functional since it accepts a parameter. Specifically, it accepts a list of selectors, whether they be simple like img or complex with combinators like img + p.

However, :has() is one of four functional pseudo-classes, with the others being :is(), :where(), and :not(). Each of them accepts a selector list with a few other unique features.

If you’ve already used :is() and :where(), it’s likely been to manage specificity. Using :is() means the selector in the list with the highest specificity gives the entire selector its weight. While using :where() lends the entire selector list zero-specificity, making it easily overruled by later rules in the cascade.

Additionally, :is() and :where() have the extra special ability to be forgiving selectors. This means you may include (purposely or not) selectors the browser doesn’t understand, and it will still process the parts it does understand. Without this forgiving behavior, the browser would discard the entire rule.

The other benefit of both :is() and :where() is to create succinct, complex selectors. This is especially handy when using combinators and affecting multiple siblings or descendants, for example, article :is(h1, h2, h3).

Our last pseudo-class, :not(), has been available in CSS for the longest. However, alongside Selectors Level 4 when :is() and :where() were released, :not() was enhanced. This happened when it was allowed to accept a list of selectors instead of a single selector. It also has the same specificity behavior noted for :is().

Finally, we need to know about an underused, incredibly powerful feature of :is(), :where(), and :not() that we’ll be using to make our advanced :has() selectors. Using the * character within these selectors — which normally in CSS is the “universal selector” — actually refers to the selector target. This allows checking the preceding siblings or ancestors of the selector target. So, in img:not(h1 + *), we’re selecting images that do not directly follow an h1. And in p:is(h2 + *), we’re selecting paragraphs only if they directly follow h2. We’ll be using this behavior for our first demo next.

Polyfill For :only-of-selector

While :only-of-type is a valid pseudo-class, it only works to select within elements of the same element type. Given .highlight:only-of-type, no matches would be made in the following HTML because the class has no effect on reducing the scope.

<p>Not highlighted</p>
<p class="highlight">.highlight</p>
<p>Not highlighted</p>

If there was only one paragraph with the highlight class within a parent, it might falsely appear to be working. But in that case, it’s because the root element type the class is attached to is a paragraph, so it matches as true since there are no sibling paragraphs.

By combining :has() and :not(), we can effectively create an :only-of-selector that will match a singleton within a range of siblings based on a class or other valid selector.

We ultimately want our selector to match when there are no matching siblings that exist before or after the target.

A strength of :has() is testing for what follows an element. Since we want to test any number of siblings that follow, we’ll use the general sibling combinator ~ to create the first condition.

.highlight:not(:has(~ .highlight)

So far, this gives us the match of “highlights that do not have sibling highlights following it.”

Now we need to check prior siblings, and we’ll use the ability of :not() on its own to add that condition.

.highlight:not(:has(~ .highlight)):not(.highlight ~  *)

The second :not() condition is an AND clause to our selector that says “AND not itself a sibling of a previous highlight.”

With that, we have polyfilled the non-existent :only-of-selector pseudo-class!

To resolve this, we need to add a complex AND condition using :not() to exclude items that are not between [data-range="end"] and [data-range="start"], in that order.

On its own, this part of the selector reads as: “do not select items that follow [data-range="end"] which also have a later sibling of [data-range="start"].”

/* Note: this needs appended on the previous selector, not used alone */
:not([data-range="end"] ~ :has(~ [data-range="start"]))

In total, this makes for an admittedly long but very powerful selector that wasn’t possible before :has() without also using JavaScript due to the previous lack of the “look ahead” and “look behind” abilities in CSS.

/* Select all between a range */
[data-range="start"] ~ :has(~ [data-range="end"]):not([data-range]):not([data-range="end"] ~ :has(~ [data-range="start"]))
Keep in mind that just like other selectors, you can use :has() when you construct a selector within JavaScript. The ability to select previous siblings, ancestors and the other features we’ve learned will also make your JS selectors more efficiently powerful!

See the Pen Multi-range element selectors with :has() [forked] by Stephanie Eckles.

Linear Range Selection Based On State

Let’s pull together some of the qualities of :has() selectors and combinators we’ve learned to make a star rating component.

The underlying “star” will be a radio input, which will give us access to a :checked state to assist in developing the selectors.

<div class="star-rating">
  <fieldset>
    <legend>Rate this demo</legend>
    <div class="stars">
      <label class="star">
        <input type="radio" name="rating" value="1">
        <span>1</span>
      </label>
      <!-- ...4 more stars -->
    </div>
  </fieldset>
</div>

As shown in the following video preview, when a user hovers over the outlined stars, then the range from the start (left-most) to the hovered star should fill in with color. On selection, when the star radio is checked, the star and labeling number scale up in size and keep the fill color. If the user hovers over stars after the checked star, the range should fill in the stars up to the hover. If the user hovers stars before the checked star, the range should fill in only up to the hovered star, and stars between the hover and previously checked star should have the fill color lightened.

That’s a lot of ranges to keep track of, but with :has(), we can break them into segmented selectors real quick!

The following selector series applies to all states where we want a star or range of stars to fill in for or up to the :checked star. The rule updates a set of custom properties that will affect the star shape, created through a combo of the ::before and ::after pseudo-elements on the label.star.

Altogether, this rule selects the range of stars between the first star and the star being hovered, or the first star and the star with a checked radio.

.star:hover,
/* Previous siblings of hovered star */
.star:has(~ .star:hover),
/* Star has a checked radio */
.star:has(:checked),
/* Previous siblings of a checked star */
.star:has(~ .star :checked) {
  --star-rating-bg: dodgerblue;
}

Next, we want to lighten the fill color of stars in the range between the star being hovered and a later checked star, and checked stars that follow the hovered star.

/* Siblings between a hovered star and a checked star */
.star:hover ~ .star:has(~ .star :checked),
/* Checked star following a hovered star */
.star:hover ~ .star:has(:checked) {
  --star-rating-bg: lightblue;
}

As far as state selectors go for our star rating component, that’s all there is to it!

The CodePen demo has a few extra tricks on how the component is created using CSS grid, custom properties, and clip-path. For accessibility, it also ensures color isn’t the only indicator by scaling up the checked star. And it handles for high contrast themes (aka “forced colors”) by supplying values from the system colors palette to ensure the :checked star fill is visible. Additionally, the transitions are shortened when a user prefers reduced motion.

See the Pen Star Rating Component with :has() [forked] by Stephanie Eckles.

Stateful Multi-Range Selection Groups

Whereas the star rating component showed a dynamic style change based on state, the availability of stateful elements also makes it easier to use :has() for creating visual boundaries.

Our earlier multi-range selectors relied on manually adding “hooks” into the markup to correctly style ranges without leaking into the in-between areas. But if we have a field set containing checkboxes, we can once again use the :checked state to clearly identify boundaries around checked and unchecked items.

In this preview video, as checkboxes are selected, they receive a border and green background to create the visual boundary. Thanks to :has(), that boundary grows to appear to wrap groups of checked items so that the visual box seems as though it's around the whole group. The first item (or a singleton) gets round top corners, and the last item (or a singleton) gets round bottom corners as well as a slight shadow.

We need to create rules to handle the top, middle, and bottom appearance based on where the item falls within the set. Single items should receive all three styles.

Our HTML is set up to wrap each checkbox input with its label, so all of our selectors will begin by matching against label:has(:checked) to see if the label contains a checked input.

To determine either the first or single item in the set, we need to add the condition that it is not following a previous item with a checked input. This rule will style the top appearance.

/* First checked item in a range
 OR top of a single checked item */
label:has(:checked):not(label:has(:checked) + label)

To determine either the last or single item in the set, we flip the previous condition to check that it is not followed by a checked input. This rule will style the bottom appearance.

/* Last checked item in a range
 OR bottom of a single checked item */
label:has(:checked):not(label:has(+ label :checked))

For the middle appearance, we’ll create a rule that actually captures the group from start to finish since all of the items in the rule should receive a background color and side borders.

We could simply use label:has(:checked) for this selector given the context. However, we’re learning how to select and style ranges, so to complete our exercise, we’ll write the expanded selectors.

The logic represented in the first selector is “select labels with checked inputs that are followed by sibling labels containing checked inputs,” which captures all but the last item in the range. For that, we repeat the selector we just created for styling the last checked item in the range.

/* Range of checked items */
label:has(:checked):has(~ label :checked),
label:has(:checked):not(label:has(+ label :checked))

This CodePen demo also shows off accent-color for changing the checked input color and uses custom properties for managing the border radius. It also uses logical properties.

See the Pen Stateful multi-range selection groups with :has() [forked] by Stephanie Eckles.

More Resources On Writing :has() Selectors

You can explore all of the demonstrations we reviewed in my CodePen collection.

Other folks have started experimenting with what’s possible using :has(), and I encourage you to check out these resources for even more ideas. As with all recently released features, the field of opportunity is wide-open, and we all benefit when we share our learnings!

Collective #741







GenCup

A generative art project that combines graphic design, football, and data. Using the statics of FIFA World Cup games to generate abstract compositions. Every match is a new poster.

Check it out


D2

D2 is a domain-specific language (DSL) that stands for Declarative Diagramming where you describe what you want diagrammed and it generates the image.

Check it out


gpu-io

A GPU-accelerated computing library for physics simulations and other mathematical calculations. Read more in this thread and check out the examples.

Check it out




What Is the Open Web?

A working definition of an Open Web and what we can strive for to building an open and sustainable internet.

Read it



FFmpeg – Ultimate Guide

This guide covers the ins and outs of FFmpeg starting with fundamental concepts and moving to media transcoding and video and audio processing along with practical example.

Read it


Score

Score is an open source, platform-agnostic, container-based workload specification.

Check it out



Infisical

An open source, end-to-end encrypted tool that lets you securely sync secrets and environment variables across your team and infrastructure.

Check it out



Act

Run your GitHub Actions locally for fast feedback and replacing your makefile.

Check it out





Collective #708





Collective 708 item image

JavascriptDB

Create low code serverless applications: Arrays and Objects operations read and write into your database.

Check it out



Collective 708 item image

Loaders

Free loaders and spinners for your next project. Built with HTML, CSS and some SVG. By Griffin Johnston.

Check it out






Collective 708 item image

SVG passthrough precision

If an SVG is imported into a design tool, then immediately exported as another SVG, how much precision is kept? What’s added, removed, or altered? Find out in this article.

Read it



Collective 708 item image

WeekToDo

WeekToDo is a free minimalist weekly planner app focused on privacy. Available for Windows, Mac, Linux or online.

Check it out



Collective 708 item image

YouTube.js

A full-featured wrapper around YouTube’s private API providing a simple and efficient way to interact with YouTube programmatically.

Check it out



Collective 708 item image

Arteater

Fun offline thing: Arteater digests your hand-drawn art and returns an animated GIF you can save and share.

Check it out





Collective 708 item image

NotepadNext

A cross-platform, reimplementation of Notepad++ available for Windows, Linux, and macOS.

Check it out




The post Collective #708 appeared first on Codrops.

Scrollbar Reflowing

This is a bit of advice for developers on Macs I’ve heard quite a few times, and I’ll echo it: go into System Preferences > General > Show scroll bars and set to always. This isn’t about you, it’s about the web. See, the problem is that without this setting on, you’ll never experience scrollbar-triggered layout shifts, but everyone else with this setting on will. Since you want to design around not causing this type of jank, you should use this setting yourself.

Here’s Stefan Judis demonstrating that usage of viewport units can be one of the causes:

There, 100vw causes horizontal overflow, because the vertical scrollbar was already in play, taking up some of that space. Feels incredibly wrong to me somehow, but here we are.

Stefan points to Kilian Valkhof’s article about dealing with this. The classic fixes:

The easy fix is to use width: 100% instead. Percentages don’t include the width of the scrollbar, so will automatically fit.

If you can’t do that, or you’re setting the width on another element, add overflow-x: hidden or overflow: hidden to the surrounding element to prevent the scrollbar.

Kilian Valkhof, “How to find the cause of horizontal scrollbars”

Those are hacks, I’d say, since they are both things that aren’t exact matches for what you were wanting to do.

Fortunately, there is an incoming spec-based solution. Bramus has the scoop:

A side-effect when showing scrollbars on the web is that the layout of the content might change depending on the type of scrollbar. The scrollbar-gutter CSS property —which will soon ship with Chromium — aims to give us developers more control over that.

Bramus Van Damme, “Prevent unwanted Layout Shifts caused by Scrollbars with the scrollbar-gutter CSS property”

Sounds like the trick, and I wouldn’t be surprised if this becomes a very common line in reset stylesheets:

body {
  scrollbar-gutter: stable both-edges;
}

That makes me wonder though… it’s the <body> when dealing with this at the whole-page level, right? Not the <html>? That’s been weird in the past with scrolling-related things.

Are we actually going to get it across all browsers? Who knows. Seems somewhat likely, but even if it gets close, and the behavior is specced, I’d go for it. Feels progressive-enhancement-friendly.


The post Scrollbar Reflowing appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

Sticky Table of Contents with Scrolling Active States

Say you have a two-column layout: a main column with content. Say it has a lot of content, with sections that requires scrolling. And let's toss in a sidebar column that is largely empty, such that you can safely put a position: sticky; table of contents over there for all that content in the main column. A fairly common pattern for documentation.

Bramus Van Damme has a nice tutorial on all this, starting from semantic markup, implementing most of the functionality with HTML and CSS, and then doing the last bit of active nav enhancement with JavaScript.

For example, if you don't click yourself down to a section (where you might be able to get away with :target styling for active navigation), JavaScript is necessary to tell where you are scrolled to an highlight the active navigation. That active bit is handled nicely with IntersectionObserver, which is, like, the perfect API for this.

Here's that result:

It reminds me of a very similar demo from Hakim El Hattab he called Progress Nav. The design pattern is exactly the same, but Hakim's version has this ultra fancy SVG path that draws itself along the way, indenting for sub nav. I'll embed a video here:

That one doesn't use IntersectionObserver, so if you want to hack on this, combine 'em!

The post Sticky Table of Contents with Scrolling Active States appeared first on CSS-Tricks.