Swirly navigation

The :has() pseudo class is really a powerhouse. There are so many cool and amazing demos being released everyday showing how it can solve everyday problems and how it can replace actions we did in JS for years. In this short article: Some extra things I look forward to when playing around with it.

About three months ago, I wrote an article about some practical uses of the :has() relational pseudo class. Because of that article and experimenting with it, I can’t stop thinking about how it can fix some everyday problems. Currently we’re still waiting for some support in Firefox and as I want to keep polyfilling to a minimum in my everyday work… I can’t wait for that to happen.

For some reason, :has() is always on my mind, not a week goes by without thinking: “ah, I could’ve done this with CSS”.

I’ve been a bit busy at the start of the year with reading, creating demos and presentations, while also releasing an article about creating high-contrast design systems with custom properties on Smashing Magazine, so I will keep things short here.

Solving how we can manage our content

Especially as a person working on a lot of content-heavy websites, I look forward to how we can manage our content a bit better by using :has(). With content mostly provided by a CMS, I find that it could help a lot with structuring content and giving more sense of freedom to clients. As a simple example, It could just be about styling of titles depending on their sibling.

/* Check if the h2 is followed by a blockquote */
h2:has(+ blockquote) {
  margin-bottom: 0;
  color: hotpink;

/* Only center the h2 if the next sibling is a cta 
and the cta itself is the last item in our content */
h2:has(+ .cta:last-child) {
  text-align: center;
  margin-top: 50px;
  margin-bottom: 0;

Better ways to structure our content or even better handle some of those crazy combinations that can happen and tackle them with :has() really seems like a much needed feature in CSS.

Here is a little (very simple) example of that:

Structuring good ol’ tables

Imagine the following use case: You need a table with two axis of headings. You want to draw a thick vertical line after the headings on the left.

Table layout with thick border on left column

This is easy, as we can just do the following:

th:first-of-type {
  border-right-width: 4px;

But suddenly, somebody decides that the first two items of the tbody should be a table heading. Imagine you have no control of the markup and now we need to find a way to only draw the thick line on the second child of the thead element. Seems like :has() to the rescue again:

thead:has(+tbody th:nth-child(2)) th:nth-child(2) {
  border-right-width: 4px;

thead:has(+tbody th:nth-child(2)) th:nth-child(1) {
  border-right-width: 1px;

It could also be about placing items sticky inside the table or some other styling you fancy. Here is a simplified demo of this idea:

Using :has() for interaction and animation

There are so many possibilities when it comes to animating things by using this pseudo class, as it makes it easier for us to animate two elements in relation to each other. I created a little pagination example with a fancy hover effect:

You can ignore the CSS counter for now. I just don’t use it as often as I should and thought I’d give it a go. The important thing here is how we animate out little orbs in relation to the element that was actually hovered.

The first thing I did was set a transform and the lightness of a hsl color with a custom property as default.

:root {
  --move-0: .6;
  --move-1: .4;
  --move-2: .2;
li a::before {
  --move: 0;
  --lightness: 18%;
  /* some other styling here */
  background: hsl(330deg 100% var(--lightness));
  transform: translateY(calc(var(--move) * -65%));
  box-shadow: rgba(50, 50, 93, 0.25) 0px 50px 100px -20px;

Then by using :has() I checked the hover (or focus) state of the element and targeted the two items before it and the two items after it. It turns to quite a heavy selector but once you get the hang of it, it’s quite easy.

li:has(+li a:is(:hover, :focus)) a::before, 
li:has(a:is(:hover, :focus)) + li a::before {
  --move: var(--move-1);
  --lightness: 31%;
a:is(:hover, :focus)::before {
  --move: var(--move-0);
  --lightness: 45%;
  box-shadow: rgba(50, 50, 93, 0.25) 0px 50px 100px -20px, rgba(0, 0, 0, 0.3) 0px 30px 60px -30px, rgba(10, 37, 64, 0.35) 0px -2px 6px 0px inset;
li:has(+li + li a:is(:hover, :focus)) a::before, 
li:has(a:is(:hover, :focus)) + li + li a::before {
  --move: var(--move-2);
  --lightness: 21%;

You could read the first part as:

If the list item has a list item next to which has an a element in it that is hovered or focussed -> style the before of this a element as follows

If the list item had an a-element inside of it with a hover or focus state, style the before pseudo of the next list item’s a-element.

In the second part you take the items next to the direct siblings and so on. Yep, CSS is starting to sound a lot like a full programming language to me.

But yes… this is only the surface

I really love these demos about :has(), there are a lot of cool ideas being made on how to control elements and the relation they have to each other. For me this really was the biggest thing released in 2022 for CSS and hopefully will have complete browser support soon.

I have used it for a bit progressive enhancement - once again, to limit polyfills in projects. If you want to check for browser support, you can do this with the following feature query:

@supports selector(:has(+*)) {
  /* this has support */

This checks for a full feature report. If you’d like to know why, I suggest you read the blogpost by Bram.us on why you want :has(+ ), not :has().

Let’s go for another awesome year of CSS in 2023 and keep making those demo’s ya’ll!

 in  css