BEM for web components

How to style web components in a world where BEM no longer makes sense.

Gav McKenzie
Gav McKenzie
How-to, Industry, Engineering

In this article

We’ve been writing a lot of web components recently. Etch has been embracing the modern web and questioning our reliance on lumbering JavaScript frameworks. Web components feel like they might reduce a lot of our need for creating components in React, Angular or Vue.

We’ve been styling components using BEM for a long time. If you’re not familiar with it, BEM is a way of formatting class names in HTML and CSS to clearly explain what that class name is targeting and its function.

Blocks, elements and modifiers

If you’d like to learn more about BEM for styling, head over to the BEM methodology site.

tl;dr BEM separates class names into: Block, Element and Modifier.

The Block is the component wrapper, the top parent of your component, like a card.

An Element is an object within the component, such as the card__body .

A Modifier is a variation of a block, or element, such as card--rounded.

The formatting for BEM has settled into a fairly common standard of a Block being the first word in the class, an Element being prefixed with two underscores and a modifier being prefixed with two hyphens. .block__element--modifier. This has not always been the case and BEM did not originally dictate this naming convention, only that you must have a naming convention to separate your blocks, elements, and modifiers.

BEM is great because:

  • It namespaces your CSS, preventing accidentally clashing with other CSS on the page.
  • It makes it instantly clear what the intention of a class is: is it styling a component, a piece of a component, or changing that component’s styles?
  • It is easy to write using nested styles in SCSS with minimal extra characters cluttering the text editor.

BEM Spanners

As always, something new comes along and challenges the way you’ve been doing things.

Enter - web components.

We’ve been wrapping up our components for usage by utilising JavaScript frameworks, such as React, Angular, or Vue. This means that, rather than having to write out a whole load of clunky HTML to use our components, we can call the component instead, which looks much neater.

<div class="card">
<div class="card__body">
[...content...]
</div>
</div>
vs
<Card>
[...content...]
</Card>

Web components bring the power of creating components to the native web, so we don’t need a giant JavaScript framework to let us create encapsulated bits of UI.

<etch-card>
[...content...]
</etch-card>

However, due to the way web components work, they don’t feel like they fit with a traditional BEM class styling approach.

Web components can be styled in 2 ways. From from the shadow DOM, using :host and class selectors, or from the light DOM, using element and ::part selectors.

What the heck is shadow DOM? If that sounds more like ninjas than nerds, read up about shadow DOM on MDN.

At this point, you either know a bit about shadow DOM, or you need to go back and read those docs above. Shadow DOM is encapsulated from a CSS point of view, styles do not penetrate in, or leak out (except in some very specific cases).

A web component has a Host element, this is the top-level tag for the component and what we would normally see as the Block in BEM. In our example above, this is the etch-card element.

Inside the component shadow DOM are regular HTML elements and even other web components. These are our Elements and they can either be accessed in CSS using a class attribute or a part attribute.

Just like in React, Angular or Vue, props are passed to web components via attributes on the Host element, for example <etch-card rounded>. The props can be used to change what is rendered in the shadow DOM.

Let’s look at the 2 styling options for web components and how we could apply traditionally formatted BEM to them.

:host and classes

Web components can contain their own CSS, applied to the element on render. If we used traditional BEM, we could render an element inside the shadow DOM to use at the Block element, or add a class to the Host element. Here’s an example in lit, a popular web component framework.

class MyElement extends LitElement {
static properties = {
class: { reflect: true }
}
connectedCallback () {
super.connectedCallback()
this.classList.add("my-element")
}
willUpdate (changedProperties) {
if (changedProperties.has("class")) {
this.classList.add("my-element");
}
}
}

This is pretty verbose for just adding a class to the base element.

To style this base class, you would think we could access it like a normal class.

static styles = css`
.my-element {
color: green;
}
`;

Unfortunately, due to the way style encapsulation works in shadow DOM, this class selector will not apply to the Host element, it would only apply if it was a child element within the shadow DOM. To apply to the host element, we have to wrap it in a :host-context selector.

static styles = css`
:host-context(.my-element) {
color: green;
}
`;

Styling Elements is a little simpler as we can access these from the component CSS using normal class selectors.

static styles = css`
:host-context(.my-element) {
color: green;
}
.my-element__body {
background: blue;
}
`;

To add a modifier, we would listen to a prop and use that to render a Modifier class on the host element.

class MyElement extends LitElement {
static properties = {
class: { reflect: true }
}
connectedCallback () {
super.connectedCallback()
this.classList.add("my-element");
if (this.variant === '2') {
this.classList.add("my-element--variant2");
}
}
willUpdate (changedProperties) {
if (changedProperties.has("class")) {
this.classList.add("my-element");
if (this.variant === '2') {
this.classList.add("my-element--variant2");
}
}
}
}

Once we’ve added these Elements and Modifiers, you can quickly see how inconsistent and messy the styles look; there’s is no uniformity to their appearance.

static styles = css`
:host-context(.my-element) {
color: green;
}
.my-element__body {
background: blue;
}
:host-context(.my-element--variant2) {
color: purple;
.my-element__body {
background: white;
}
}
`;

The easy-to-read nature of Block, Element and Modifier is masked by the :host-context wrappers that make it harder to differentiate what is a Block vs a Modifier, not to mention the awkwardness of maintaining a stable class on the Host element.

Adding a class to the first element inside is easier, but now what do we do with the styles on the Host element? Especially when the layout will potentially be affected by its parent layout model (grid, flexbox, etc).

class MyElement extends LitElement {
render() {
return html`
<div class="my-element">
[...content...]
</div>
`;
}
}

We could ignore the Host element by adding display: contents, but that comes with its own set of gotchas and we are still needlessly adding extra markup. It also makes it difficult to select the element when using DevTools for debugging.

Elements and ::parts

Another option for styling web components is to not include any CSS in the web component and instead expose parts of the component for styling using the part attribute. This can be a pro or a con because now we can apply the CSS to at least the host element of the web component before the JavaScript has kicked in to initialise it, but we have to keep our CSS build somewhere separate from the component as it will not be self-styling.

If the web component applies a Block class to itself as shown above, styling it from the light DOM is easy, as we can use normal CSS.

.my-element {
color: purple;
}

The awkward part is that we cannot touch the components’ shadow DOM classes from our light DOM CSS.

.my-element__body {
background: blue; /* Will not be applied */
}

The only way we can style the inner shadow DOM is if the component adds a part attribute to the element we want to style.

class MyElement extends LitElement {
render() {
return html`
<div class="my-element">
<div class="my-element__body" part="my-element-body">
[...content...]
</div>
</div>
`;
}
}

Now we can access the element within, but we’re still blocked from using our standard BEM syntax because we can’t use the class selector.

.my-element {
color: purple;
::part(my-element-body) {
color: blue;
}
}

Now, this CSS is not traditional BEM, but it actually looks OK. It feels a bit more legible than the weird host context mixed classes mess, but we still have the problem of applying the Block class with JavaScript and having to maintain that, as well as adding Modifier classes.

But this does lead us to…

BEM for web components.

If you remember, back at the start, we said that BEM does not mean double underscore and double hyphen, it just means defining a selector format for determining if a CSS selector is for a Block and Element or a Modifier.

Our selectors should:

  • Namespace our styles so we don’t get clashes
  • Be simple to apply to components and elements within
  • Be easy to read and fast to determine which selector type is which

We still have the option of light DOM vs shadow DOM CSS, which leads us to 2 main approaches.

Shadow DOM

:host .element [modifier]

In shadow DOM CSS we have a special selector called a :host selector, which applies to the Host element of the component. This prevents us needing to add a class to the base element and removes the need to a :host-context selector to wrap it. The :host element is always the BEM Block.

Shadow DOM CSS is encapsulated so class selectors do not leak in or out from the light DOM. This means there is no need to namespace our Element selectors with the block name. We can use pure, simple, class selectors, such as .body.

Because component props are passed as attributes, we can use attribute selectors as our Modifiers. There is no need to convert the attribute to a class with JavaScript for use in CSS because the attribute already exists.

class MyElement extends LitElement {
static styles = css`
:host {
color: green;
}
.body {
background: blue;
}
:host-context([rounded]) {
border-radius: 20px;
}
`;
@property({ reflect: true }) rounded?: boolean;
render() {
return html`
<div class="body">
[...content...]
</div>
`;
}
}

Light DOM

custom-element ::part [modifier]

Something about rendering all our HTML and CSS using JavaScript makes my progressive enhancement nerve twitch so we’re always looking for ways to keep JavaScript out and use the least tech possible. Keeping JavaScript out of CSS is always appealing and using light DOM CSS and part selectors lets us move (at least some) styling over to pure CSS.

In this scenario, the Block selector becomes the custom element selector, such as my-element, where we select it just like any other HTML element, such as a button or an h1.

Elements become ::part selectors by adding part attributes to elements in the shadow DOM.

Just like in the shadow DOM example, we can use attribute selectors as our Modifiers.

Because CSS nesting is now supported fairly widely, this approach to styling web components gives us quite a familiar layout comparable to traditional BEM styling in SCSS.

my-element {
color: purple;
::part(body) {
background: blue;
}
&[rounded] {
border-radius: 20px;
}
}

tl;dr - the solution

Don’t get trapped in traditional .block__element--modifier, optimise your selectors for your tech today.

:host .element [modifier] for shadow DOM

and

custom-element ::part [modifier] for light DOM

Are both great approaches to clean, modern, styling of native web components.

Etch is a web software consultancy based in the UK©2012-2024 Etch Software Ltd - Policies