How to use web components :host selector?

If you’re new to writing vanilla web components, the :host selector in web components can be a bit tricky and confusing. However, fear not! In this post, I will cover all the different scenarios where you might use the :host selector, so you can become a pro at using it in your web components.

Please note that if you’re new to web components, this article may not be the best fit for you. Instead, I recommend checking out some web components tutorials on MDN to get started. Once you have a basic understanding of web components, feel free to come back and dive deeper into the topic with this article.

Let’s start by examining a web component like this one shown below.

index.html
<fancy-button>
  #shadow-root(open)
    <button><slot></slot></button>
    <just-a-spinner></just-a-spinner>
    <style>/* will add css rules here to style this component */</style>
</fancy-button>

The slot element in this example allows us to use this custom element in our web page like this.

<fancy-button>Click me</fancy-button>

Once the browser has defined this custom element, the text “Click me” will be treated as the textContent of our slot element. By the way, the slot element is display: contents by default, so you can treat the button as the parent wrapper of the text “Click me”. I hope that make sense.

Now, let’s move on to the main event: the :host selector.

1. The basics

/* valid rule */
:host {
  display: flex;
}

/* invalid rule */
fancy-button {
  display: flex;
}

Inside the shadow root of our custom element fancy-button, we can’t use the fancy-button CSS tag selector to target our custom element, as styles inside the shadow root are encapsulated. This is where the :host selector comes to the rescue.

One thing to note is that a custom element is an inline element by default. However, I’ve found that most of the time, the default inline behavior is not really what we want.

2. :host with class

If we want to target fancy-button that has a visible class, we need to add the visible class inside parentheses, like this. This will ensure that the visible class is properly applied to the fancy-button element.

<fancy-button class="visible">Click me</fancy-button>
/* hide initially */
:host {
  display: none;
}

/* if it has a 'visible' class, show this button */
:host(.visible) {
  display: block;
}

3. :host with attributes

Just like with classes, attributes also need to be enclosed in parentheses when using them as selectors. For example, if we want to select all fancy-button elements with a theme="round" attribute.

<fancy-button theme="round">Click me</fancy-button>
:host([theme="round"]) {
  border-radius: 2em;
}

In some cases, we may want users to be able to consume our button components with multiple theme attributes.

<fancy-button theme="round large">Click me</fancy-button>

As a component author, we can use the handy ~ CSS operator to target specific theme attributes. This operator selects elements that have the specified attribute with a value that contains a given word, delimited by spaces.

:host([theme~="round"]) {
  border-radius: 2em;
}

:host([theme~="large"]) {
  font-size: 1.2rem;
}

/* or want to styles button differently */
:host([theme~="round"]) button {
  /* styles here */
}

:host([theme~="large"]) button {
  /* styles here */
}

4. How to work with :hover :focus :focus-within state?

For element state like :hover, :focus and :focus-within, they also need to be enclosed in parentheses when used as selectors.

:host(:hover) {
  background: white;
}

:host([theme="purple"]:hover) {
  background: purple;
}

:host(:focus-within) {
  border: 1px solid white;
}

:host([theme="purple"]:focus-within) {
  border-color: purple;
}

5. What about ::before and ::after suedo-elements?

For ::before and ::after suedo-elements, they need to be outside of parentheses when used as selectors.

:host::before {
  content: attr(data-content);
}

:host([theme="purple"])::before {
  border: 1px solid purple;
}

6. Working with :is selector

We can simply CSS code with :is() selector to match multiple selectors in the same rule. In this example, the styles are applied to a button element, but only when its host element has a theme attribute value of primary or large.

:host(:is([theme="primary"], [theme="large"])) button {
  /* styles here */
}