Some little ways I’m using CSS :has() in the real world
I’ve created some low fidelity demos of :has() snippets that I’ve been using in real-world client projects.
There’s a lot of chatter around the new(ish) :has()
pseudo-class. It’s something we’ve been crying out for, for years: being able to select parent elements!
A useful mental model for :has()
is that you are querying the parent’s children’s state and/or presence rather than selecting the parent from the children themselves. I like that. It makes a lot of sense.
I’m not 100% convinced :has()
is the silver bullet others might claim it is though. I personally still utilise CUBE exceptions more regularly, but I am also in the privileged position where projects I work on in the studio don’t restrict access to the markup. I see :has()
as being more useful for little tweaks more than anything, but if you don’t have access to markup, it really is a silver bullet.
With all that in mind, I thought I’d produce some low fidelity examples of how I’ve been using :has()
lately on proper client projects to give you some real world stuff to look at. Let’s dig in.
Banner layout adjustments permalink
In a design system we work on for a client, there’s a pretty straightforward banner. This was recently updated to be dismissible, so we had to create a new variant to the pattern.
The only difference to the default pattern though is there’s a <button>
element present, so a quick :has()
query later, we could apply a flex layout in a jiffy.
.banner {
background: var(--color-primary);
color: var(--color-light);
font-weight: var(--font-bold);
text-align: center;
}
.banner:has(button) {
display: flex;
justify-content: space-between;
gap: var(--space-s);
text-align: revert;
}
Flex labels with input children permalink
The context for this is that I like the following pattern for labels because I like to keep them as inline
elements, but form fields that follow them should break on to a new line.
label::after {
content: "\A";
white-space: pre;
}
For labels that contain inputs like checkboxes and radios though, it’s useful to render those as flexbox layouts. Historically, that would require a class being added (or several in ASS codebases), but now, I’ve updated our global styles to this instead.
label:has(input) {
display: flex;
align-items: flex-start;
gap: var(--space-s);
}
The reason I only look for input
is because I never put text inputs inside labels. If you do that, you should update the selector to this instead: label:has(:is(input[type="checkbox"], input[type="radio"]))
.
Highlight parent elements when their children are targeted permalink
This one is super quick and super simple. If you’ve got an element with an id
, you can trigger its :target
state by appending it’s id to the URL with a #
, like this: https://example.com/#my-element
.
Historically, you couldn’t apply styles to an element’s parent when it’s targeted, but now you can with :has()
.
section:has(:target) {
background: var(--color-light-shade);
border: 2px solid var(--color-primary);
}
Handy as heck.
Dimmer siblings when an element is hovered permalink
It’s a design pattern that’s been on the web forever. The idea is when you hover an element its siblings all dim, so the user’s visual focus is more targeted.
We’ve been able to do this with CSS forever too, but the selectors to achieve the effect were pretty gnarly. Quite a few approaches resulted in flickering or all items dimmed if your pointer accidentally found itself in gutters too.
It’s not the case any more with :has()
though!
.tiles:has(:hover) .tile:not(:hover) {
opacity: 70%;
}
The beauty of this selector is it’s really clear what’s going on too.
Yeh, there’s nothing complicated or fancy in this article, but I just wanted to show some handy real-world ways to use :has()
. If you really want to get into :has()
, I strongly recommend checking out Ahmad’s interactive guide. It’s fantastic!
P.S. one last little trick. On this site, paragraphs in the .post
block are limited to 60ch
as a max-width
. That's not ideal for demos though, so…
.post p:has(code-pen) {
max-width: unset;
}
Easy peasy 🙂