DEV Community

Cory LaViska
Cory LaViska

Posted on • Edited on • Originally published at abeautifulsite.net

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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> */
}
Enter fullscreen mode Exit fullscreen mode

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 */
}
Enter fullscreen mode Exit fullscreen mode

This also works with parts, too.

::part(image):hover {
  /* targets the image part's hover state */
}
Enter fullscreen mode Exit fullscreen mode

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 */
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Now we can target the loaded state using ::part()!

::part(image--loaded) {
  /* targets the image once it has loaded */
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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">
Enter fullscreen mode Exit fullscreen mode

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 */
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)