CSS Parts Inspired by BEM
In a previous post, I explored valid names for CSS parts and discovered that there are very few restrictions in what you can call them. The purpose of that deep dive was to help identify a pattern for naming parts that lets me expose states and subparts, or parts exported as a result of composition.
Using inspiration from BEM, I've settled on a familiar and intuitive pattern that I'd like to share.
Blocks → Parts #
In BEM terms, a block "encapsulates a standalone entity that is meaningful on its own." Block names consist only of Latin letters, numbers, and dashes. This translates well to CSS parts.
Consider the following custom element template. It's contrived, as its only purpose is to render an image.
<template>
<!-- shadow root -->
<img part="image" src="..." alt="...">
</template>
If we wanted to make a more descriptive name, we could have called the part user-provided-image
or something, as long as we stick to letters, numbers, and dashes.
Elements → Subparts #
In BEM, elements are "parts of a block [that] have no standalone meaning. Any element is semantically tied to its block." An example looks like this.
<div class="block">
...
<span class="block__elem"></span>
</div>
Note the two underscores separating the block from the element. You might be wondering how this ties into CSS parts. Since parts are unique to the shadow root, we don't need to namespace them to prevent collisions. Two different custom elements can have two different parts with the same name and that's totally fine.
However, when a custom element is nested inside another custom element, it's often desirable to expose the nested element and its parts, otherwise, consumers won't be able to target it fully with ::part()
.* This means we need to expose the nested element with the part
attribute and its parts with the exportparts
attribute.
Let's evolve our example so it contains a nested a custom element called <my-image>
, and let's assume that <my-image>
has two parts called photo
and caption
.
<template>
<!-- shadow root -->
<my-image
part="image"
exportparts="
photo:image__photo,
caption:image__caption
"
src="..."
alt="..."
>
...
<my-image>
</template>
You can see that I've exposed the host element for styling with part="image"
, which follows the "block" naming convention. Now take a look at the exportparts
attribute. Conveniently, we can rename subparts when we export them. This lets us avoid collisions (e.g. what if the host element and the nested element have parts of the same name?).
In this example, the host element is exposed through the image
part, and its photo
and caption
subparts are exposed as image__photo
and image__caption
, respectively. Notice how everything is scoped to the image
block now?
End users can now use a very familiar syntax for targeting the nested element and all its parts in their CSS.
::part(image) {
/* matches the nested <my-image> element */
}
::part(image__photo) {
/* matches the subpart named photo in <my-image> */
}
::part(image__caption) {
/* matches the subpart named caption in <my-image> */
}
It's not uncommon for custom element authors to neglect to export parts. At the time of this writing, exportparts
seems to be one of the lesser known features of web components, but it's well-supported and incredibly powerful.
Anyways, this is feeling pretty good so far!
Modifiers → States #
Element state is a pretty simple concept. If you have a button, it can have a hover state, a focus state, an active state, etc. Normally, we can target such states with CSS using pseudo selectors.
button:hover {
/* targets the button's hover state */
}
This also works with parts, too.
::part(image):hover {
/* targets the image part's hover state */
}
But not all states are available to target with pseudo selectors, and what if you want to add custom states? More often than not, custom element authors lean on host element attributes for this.
my-image[loaded] {
/* targets the host element when the image has loaded successfully */
}
my-image[error] {
/* targets the host element when the image fails to load */
}
While this works, mapping stateful parts to attributes on the host element isn't an elegant solution. Let's see how we can improve our example using stateful parts and a BEM-like syntax. In BEM, a modifier is used "to change appearance, behavior or state" and is delimited by two dashes.
Fortunately, parts are designed to work a lot like classes. In fact, they use the same DOMTokenList API as classList
. This means elements can have more than one part, and part names can be reused throughout the custom element's template!
Evolving our example further, we can add modifier parts to indicate various states. Let's imagine the image in our example has loaded successfully. We can indicate this by adding the image--loaded
part.
<template>
<!-- shadow root -->
<my-image
part="image image--loaded"
exportparts="..."
src="..."
alt="..."
>
...
<my-image>
</template>
Now we can target the loaded state using ::part()
!
::part(image--loaded) {
/* targets the image once it has loaded */
}
There's no limit to the number of parts an element can have. You can add many additional states if you think they'll be useful.
<template>
<!-- shadow root -->
<my-image
part="
image
image--loaded
image--square
image--large
image--jpeg
"
exportparts="..."
src="..."
alt="..."
>
...
<my-image>
</template>
Why BEM? #
While the examples herein are contrived, I'm hoping you can see the value in using the BEM convention for naming CSS parts. I chose it because it's familiar and it easily represents everything we need: parts, subparts, and states.
Another big win for BEM-inspired part names is that consumers don't have to escape anything in their CSS. It's perfectly valid to name a part image:loaded
, for example.
<div part="image image:loaded">
But your users will need to escape the colon in their stylesheet, otherwise the selector won't match.
::part(image\:loaded) {
/* this works, but requires a backslash before the colon */
}
This may not seem like a big deal but, in the world of CSS, escaping isn't something users typically do and they're probably going to forget. Imagine how frustrating it will be for a user to see a part called image:loaded
in your documentation and, when they try to implement it, it doesn't work and they don't know why.
Since dashes and underscores don't need to be escaped, they're a more foolproof choice for naming parts.
*The ::part()
selector is intentionally limited by the spec so you can only target elements the custom element author explicitly exposes.