I have been experimenting with the new :has() selector introduced in CSS Selectors Level 4. The powerful feature of this selector is that it allows you to ‘look back’ through the DOM tree to select parent elements. This post demonstrates how :has() can solve a problem I was having when putting multiple pictures adjacent to one another in a post.


Since I started making full use of the picture element, I’ve mentally separated pictures (photos) from images (graphics).1 The separation leaves me free to embellish pictures with borders or shadows, without assigning classes to everything. I use the mask-clip property to give all pictures bevelled edges.

The effect typically looks like this:

single_div

Now let’s put two of them on top of each other in the document.

two_divs_bevelled

You see the problem? The clip mask applies to each block individually, not to the two blocks together. The corners are bevelled where the two blocks meet.

Prior to :has() you could remove the top bevel from the bottom block using the ‘+’ selector as picture + picture, but there was no way to deal with the top block.

Here’s how you can select the top block.

picture:has(+ picture)

The bottom picture follows a picture element above it. The top picture has a picture element below it (which follows a picture element above it). And they have flat borders where they meet.

two_divs_flush

Let’s try a three-block sequence, which means selecting a middle block with one above and one below.

picture + picture:has(+ picture)

The middle picture follows a picture element above it, and it has a picture element below it (which follows a picture element above it). The styling for the middle case is pretty easy: just set top and bottom margins/padding to zero, and remove the clip mask completely.

three_divs_flush

Bringing it all together, here’s the CSS currently applied to this site.2

/* top image */
 p:has(+ p a picture) img {
     clip-path: polygon(3% 0, 97% 0, 100% 3%, 100% 100%, 0 100%, 0 3%);
     margin-bottom: 0;
     padding-bottom: 0;
}
/* middle image */
 p:has(a picture) + p:has(+ p a picture) img {
     clip-path: none;
     margin-top: 0;
     margin-bottom: 0;
     padding-top: 0;
     padding-bottom: 0;
}
/* bottom image */
 p:has(a picture) + p:has(a picture) img {
     clip-path: polygon(100% 0, 100% 97%, 97% 100%, 3% 100%, 0 97%, 0 0);
     margin-top: 0;
     padding-top: 0;
}
/* remove paragraph margins between pictures */
 p:has(a picture) + p:has(a picture) {
     margin-top: 0;
}
 p:has(+ p a picture){
     margin-bottom: 0;
}

These are very heavy selectors, which is useful in the sense that they naturally over-ride the styling of any single lone image in a paragraph. I haven’t delved much into specificity and layers, but there’s no need to fight the cascade here.

Firefox support

:Has() is currently supported by most modern browsers… all except for Firefox. Support is being worked on, but as something which introduces a whole lot of new logic to CSS, it comes with a lot of bugs and inconsistencies. For now you can try out the experimental implementation by setting layout.css.has-selector.enabled to true in about:config.

  1. For example, the images in this post are in plain image tags and so not subject to the CSS effects which apply to pictures. The images are SVGs translated from divs in a test webpage I made to try this out. CSS on the page only applies to inline SVGs, not to SVGs linked in an image tag. 

  2. In case this clears up some confusion, the mask-clip property is applied to the image inside the picture tag. The image is the content, the picture is the container, and in this case the picture is also (usually) surrounded by a link and separated by a paragraph.