Web Component Pseudo-Classes and Pseudo-Elements are Easier Than You Think
We’ve discussed a lot about the internals of using CSS in this ongoing series on web components, but there are a few special pseudo-elements and pseudo-classes that, like good friends, willingly smell your possibly halitotic breath before you go talk to that potential love interest. You know, they help you out when you need it most. And, like a good friend will hand you a breath mint, these pseudo-elements and pseudo-classes provide you with some solutions both from within the web component and from outside the web component — the website where the web component lives.
I’m specifically referring to the ::part
and ::slotted
pseudo-elements, and the :defined
, :host
, and :host-context
pseudo-classes. They give us extra ways to interact with web components. Let’s examine them closer.
Article series
- Web Components Are Easier Than You Think
- Interactive Web Components Are Easier Than You Think
- Using Web Components in WordPress is Easier Than You Think
- Supercharging Built-In Elements With Web Components “is” Easier Than You Think
- Context-Aware Web Components Are Easier Than You Think
- Web Component Pseudo-Classes and Pseudo-Elements are Easier Than You Think (You are here)
The ::part
pseudo-element
::part
, in short, allows you to pierce the shadow tree, which is just my Lord-of-the-Rings-y way to say it lets you style elements inside the shadow DOM from outside the shadow DOM. In theory, you should encapsulate all of your styles for the shadow DOM within the shadow DOM, i.e. within a element in your
element.
So, given something like this from the very first part of this series, where you have an
in your
, your styles for that
should all be in the
element.
<template id="zprofiletemplate">
<style>
h2 {
font-size: 3em;
margin: 0 0 0.25em 0;
line-height: 0.8;
}
/* other styles */
</style>
<div class="profile-wrapper">
<div class="info">
<h2>
<slot name="zombie-name">Zombie Bob</slot>
</h2>
<!-- other zombie profile info -->
</div>
</template>
<template id="zprofiletemplate">
<style>
h2 {
font-size: 3em;
margin: 0 0 0.25em 0;
line-height: 0.8;
}
/* other styles */
</style>
<div class="profile-wrapper">
<div class="info">
<h2>
<slot name="zombie-name">Zombie Bob</slot>
</h2>
<!-- other zombie profile info -->
</div>
</template>
But sometimes we might need to style an element in the shadow DOM based on information that exists on the page. For instance, let’s say we have a page for each zombie in the undying love system with matches. We could add a class to profiles based on how close of a match they are. We could then, for instance, highlight a match’s name if he/she/it is a good match. The closeness of a match would vary based on whose list of potential matches is being shown and we won’t know that information until we’re on that page, so we can’t bake the functionality into the web component. Since the
is in the shadow DOM, though, we can’t access or style it from outside the shadow DOM meaning a selector of zombie-profile h2
on the matches page won’t work.
But, if we make a slight adjustment to the markup by adding a
part
attribute to the
:
<template id="zprofiletemplate">
<style>
h2 {
font-size: 3em;
margin: 0 0 0.25em 0;
line-height: 0.8;
}
/* other styles */
</style>
<div class="profile-wrapper">
<div class="info">
<h2 part="zname">
<slot name="zombie-name">Zombie Bob</slot>
</h2>
<!-- other zombie profile info -->
</div>
</template>
<template id="zprofiletemplate">
<style>
h2 {
font-size: 3em;
margin: 0 0 0.25em 0;
line-height: 0.8;
}
/* other styles */
</style>
<div class="profile-wrapper">
<div class="info">
<h2 part="zname">
<slot name="zombie-name">Zombie Bob</slot>
</h2>
<!-- other zombie profile info -->
</div>
</template>
Like a spray of Bianca in the mouth, we now have the superpowers to break through the shadow DOM barrier and style those elements from outside of the :
/* External stylesheet */
.high-match::part(zname) {
color: blue;
}
.medium-match::part(zname) {
color: navy;
}
.low-match::part(zname) {
color: slategray;
}
There are lots of things to consider when it comes to using CSS ::part
. For example, styling an element inside of a part is a no-go:
/* frowny-face emoji */
.high-match::part(zname) span { ... }
But you can add a part
attribute on that element and style it via its own part name.
What happens if we have a web component inside another web component, though? Will ::part
still work? If the web component appears in the page’s markup, i.e. you’re slotting it in, ::part
works just fine from the main page’s CSS.
<zombie-profile class="high-match">
<img slot="profile-image" src="https://assets.codepen.io/1804713/leroy.png" />
<span slot="zombie-name">Leroy</span>
<zombie-details slot="zdetails">
<!-- Leroy's details -->
</zombie-details>
</zombie-profile>
But if the web component is in the template/shadow DOM, then ::part
cannot pierce both shadow trees, just the first one. We need to bring the ::part
into the light… so to speak. We can do that with an exportparts
attribute.
To demonstrate this we’ll add a “watermark” behind the profiles using a web component. (Why? Believe it or not this was the least contrived example I could come up with.) Here are our templates: (1) the template for , and (2) the same template for
but with added a
element on the end.
<template id="zwatermarktemplate">
<style>
div {
text-transform: uppercase;
font-size: 2.1em;
color: rgb(0 0 0 / 0.1);
line-height: 0.75;
letter-spacing: -5px;
}
span {
color: rgb( 255 0 0 / 0.15);
}
</style>
<div part="watermark">
U n d y i n g L o v e U n d y i n g L o v e U n d y i n g L o v e <span part="copyright">©2 0 2 7 U n d y i n g L o v e U n L t d .</span>
<!-- Repeat this a bunch of times so we can cover the background of the profile -->
</div>
</template>
<template id="zprofiletemplate">
<style>
::part(watermark) {
color: rgb( 0 0 255 / 0.1);
}
/* More styles */
</style>
<!-- zombie-profile markup -->
<zombie-watermark exportparts="copyright"></zombie-watermark>
</template>
<style>
/* External styles */
::part(copyright) {
color: rgb( 0 100 0 / 0.125);
}
</style>
Since ::part(watermark)
is only one shadow DOM above the , it works fine from within the
’s template styles. Also, since we’ve used
exportparts="copyright"
on the , the copyright part has been pushed up into the
‘s shadow DOM and
::part(copyright)
now works even in external styles, but ::part(watermark)
will not work outside the ’s template.
We can also forward and rename parts with that attribute:
<zombie-watermark exportparts="copyright: cpyear"></zombie-watermark>
/* Within zombie-profile's shadow DOM */
/* happy-face emoji */
::part(cpyear) { ... }
/* frowny-face emoji */
::part(copyright) { ... }
Structural pseudo-classes (:nth-child
, etc.) don’t work on parts either, but you can use pseudo-classes like :hover
. Let’s animate the high match names a little and make them shake as they’re lookin’ for some lovin’. Okay, I heard that and agree it’s awkward. Let’s… uh… make them more, shall we say, noticeable, with a little movement.
.high::part(name):hover {
animation: highmatch 1s ease-in-out;
}
The ::slotted
pseudo-element
The ::slotted
CSS pseudo-element actually came up when we covered interactive web components. The basic idea is that ::slotted
represents any content in a slot
in a web component, i.e. the element that has the slot
attribute on it. But, where ::part
pierces through the shadow DOM to make a web component’s elements accessible to outside styles, ::slotted
remains encapsulated in the element in the component’s
and accesses the element that’s technically outside the shadow DOM.
In our component, for example, each profile image is inserted into the element through the
slot="profile-image"
.
<zombie-profile>
<img slot="profile-image" src="photo.jpg" />
<!-- rest of the content -->
</zombie-profile>
That means we can access that image — as well as any image in any other slot — like this:
::slotted(img) {
width: 100%;
max-width: 300px;
height: auto;
margin: 0 1em 0 0;
}
Similarly, we could select all slots with ::slotted(*)
regardless of what element it is. Just beware that ::slotted
has to select an element — text nodes are immune to ::slotted
zombie styles. And children of the element in the slot are inaccessible.
The :defined
pseudo-class
:defined
matches all defined elements (I know, surprising, right?), both built-in and custom. If your custom element is shuffling along like a zombie avoiding his girlfriend’s dad’s questions about his “living” situation, you may not want the corpses of the content to show while you’re waiting for them to come back to life errr… load.
You can use the :defined
pseudo-class to hide a web component before it’s available — or “defined” — like this:
:not(:defined) {
display: none;
}
You can see how :defined
acts as a sort of mint in the mouth of our component styles, preventing any broken content from showing (or bad breath from leaking) while the page is still loading. Once the element’s defined, it’ll automatically appear because it’s now, you know, defined and not not defined.
I added a setTimeout
of five seconds to the web component in the following demo. That way, you can see that elements are not shown while they are undefined. The
and the
that holds the
components are still there. It’s just the
web component that gets display: none
since they are not yet defined.
The :host
pseudo-class
Let’s say you want to make styling changes to the custom element itself. While you could do this from outside the custom element (like tightening that N95), the result would not be encapsulated, and additional CSS would have to be transferred to wherever this custom element is placed.
It’d be very convenient then to have a pseudo-class that can reach outside the shadow DOM and select the shadow root. That CSS pseudo-class is :host
.
In previous examples throughout this series, I set the
width from the main page’s CSS, like this:
zombie-profile {
width: calc(50% - 1em);
}
With :host
, however, I can set that width from inside the web component, like this:
:host {
width: calc(50% - 1em);
}
In fact, there was a div with a class of .profile-wrapper
in my examples that I can now remove because I can use the shadow root as my wrapper with :host
. That’s a nice way to slim down the markup.
You can do descendant selectors from the :host
, but only descendants inside the shadow DOM can be accessed — nothing that’s been slotted into your web component (without using ::slotted
).
That said, :host
isn’t a one trick zombie. It can also take a parameter, e.g. a class selector, and will only apply styling if the class is present.
:host(.high) {
border: 2px solid blue;
}
This allows you to make changes should certain classes be added to the custom element.
You can also pass pseudo-classes in there, like :host(:last-child)
and :host(:hover)
.
The :host-context
pseudo-class
Now let’s talk about :host-context
. It’s like our friend :host()
, but on steroids. While :host
gets you the shadow root, it won’t tell you anything about the context in which the custom element lives or its parent and ancestor elements.
:host-context
, on the other hand, throws the inhibitions to the wind, allowing you to follow the DOM tree up the rainbow to the leprechaun in a leotard. Just note that at the time I’m writing this, :host-context
is unsupported in Firefox or Safari. So use it for progressive enhancement.
Here’s how it works. We’ll split our list of zombie profiles into two divs. The first div will have all of the high zombie matches with a .bestmatch
class. The second div will hold all the medium and low love matches with a .worstmatch
class.
<div class="profiles bestmatch">
<zombie-profile class="high">
<!-- etc. -->
</zombie-profile>
<!-- more profiles -->
</div>
<div class="profiles worstmatch">
<zombie-profile class="medium">
<!-- etc. -->
</zombie-profile>
<zombie-profile class="low">
<!-- etc. -->
</zombie-profile>
<!-- more profiles -->
</div>
Let’s say we want to apply different background colors to the .bestmatch
and .worstmatch
classes. We are unable to do this with just :host
:
:host(.bestmatch) {
background-color: #eef;
}
:host(.worstmatch) {
background-color: #ddd;
}
That’s because our best and worst match classes are not on our custom elements. What we want is to be able to select the profiles’s parent elements from within the shadow DOM. :host-context
pokes past the custom element to match the, er, match classes we want to style.
:host-context(.bestmatch) {
background-color: #eef;
}
:host-context(.worstmatch) {
background-color: #ddd;
}
Well, thanks for hanging out despite all the bad breath. (I know you couldn’t tell, but above when I was talking about your breath, I was secretly talking about my breath.)
How would you use ::part
, ::slotted
, :defined
, :host
, and :host-context
in your web component? Let me know in the comments. (Or if you have cures to chronic halitosis, my wife would be very interested in to hear more.)
Web Component Pseudo-Classes and Pseudo-Elements are Easier Than You Think originally published on CSS-Tricks. You should get the newsletter.
components are still there. It’s just the
web component that gets display: none
since they are not yet defined.
The :host
pseudo-class
Let’s say you want to make styling changes to the custom element itself. While you could do this from outside the custom element (like tightening that N95), the result would not be encapsulated, and additional CSS would have to be transferred to wherever this custom element is placed.
It’d be very convenient then to have a pseudo-class that can reach outside the shadow DOM and select the shadow root. That CSS pseudo-class is :host
.
In previous examples throughout this series, I set the width from the main page’s CSS, like this:
zombie-profile {
width: calc(50% - 1em);
}
With :host
, however, I can set that width from inside the web component, like this:
:host {
width: calc(50% - 1em);
}
In fact, there was a div with a class of .profile-wrapper
in my examples that I can now remove because I can use the shadow root as my wrapper with :host
. That’s a nice way to slim down the markup.
You can do descendant selectors from the :host
, but only descendants inside the shadow DOM can be accessed — nothing that’s been slotted into your web component (without using ::slotted
).
That said, :host
isn’t a one trick zombie. It can also take a parameter, e.g. a class selector, and will only apply styling if the class is present.
:host(.high) {
border: 2px solid blue;
}
This allows you to make changes should certain classes be added to the custom element.
You can also pass pseudo-classes in there, like :host(:last-child)
and :host(:hover)
.
The :host-context
pseudo-class
Now let’s talk about :host-context
. It’s like our friend :host()
, but on steroids. While :host
gets you the shadow root, it won’t tell you anything about the context in which the custom element lives or its parent and ancestor elements.
:host-context
, on the other hand, throws the inhibitions to the wind, allowing you to follow the DOM tree up the rainbow to the leprechaun in a leotard. Just note that at the time I’m writing this, :host-context
is unsupported in Firefox or Safari. So use it for progressive enhancement.
Here’s how it works. We’ll split our list of zombie profiles into two divs. The first div will have all of the high zombie matches with a .bestmatch
class. The second div will hold all the medium and low love matches with a .worstmatch
class.
<div class="profiles bestmatch">
<zombie-profile class="high">
<!-- etc. -->
</zombie-profile>
<!-- more profiles -->
</div>
<div class="profiles worstmatch">
<zombie-profile class="medium">
<!-- etc. -->
</zombie-profile>
<zombie-profile class="low">
<!-- etc. -->
</zombie-profile>
<!-- more profiles -->
</div>
Let’s say we want to apply different background colors to the .bestmatch
and .worstmatch
classes. We are unable to do this with just :host
:
:host(.bestmatch) {
background-color: #eef;
}
:host(.worstmatch) {
background-color: #ddd;
}
That’s because our best and worst match classes are not on our custom elements. What we want is to be able to select the profiles’s parent elements from within the shadow DOM. :host-context
pokes past the custom element to match the, er, match classes we want to style.
:host-context(.bestmatch) {
background-color: #eef;
}
:host-context(.worstmatch) {
background-color: #ddd;
}
Well, thanks for hanging out despite all the bad breath. (I know you couldn’t tell, but above when I was talking about your breath, I was secretly talking about my breath.)
How would you use ::part
, ::slotted
, :defined
, :host
, and :host-context
in your web component? Let me know in the comments. (Or if you have cures to chronic halitosis, my wife would be very interested in to hear more.)
Web Component Pseudo-Classes and Pseudo-Elements are Easier Than You Think originally published on CSS-Tricks. You should get the newsletter.