DEV Community

James Garbutt
James Garbutt

Posted on

Using CSS shadow parts in web components

CSS Parts are now supported in most modern browsers, so this is a brief write up on what they're useful for and how to use them.

Shadow DOM & styling

Let's say we have an example-button component, used like so:

<example-button>Some text</example-button>
Enter fullscreen mode Exit fullscreen mode

Its inner shadow DOM may look like this:

<style>
  :host {
    display: inline-block;
  }
  button {
    color: hotpink;
  }
</style>
<button>
  <slot></slot>
</button>
Enter fullscreen mode Exit fullscreen mode

This is a fairly atomic component in that it is fairly low level and can't be split up into further components.

In this case, we can style the example-button element itself through regular CSS selectors:

example-button {
  background: cyan;
  margin: 1em;
}
Enter fullscreen mode Exit fullscreen mode

However, what if we want to change the text colour? You see we wanted a default colour of hotpink but this now means we can't override it from outside.

This is solved through CSS custom properties:

button {
  color: var(--example-button-colour, hotpink);
}
Enter fullscreen mode Exit fullscreen mode

Now it will default to hotpink but allow us to override it like so:

example-button {
  --example-button-color: green;
}
Enter fullscreen mode Exit fullscreen mode

This solves most cases where we want to give consumers the ability to style some of our component's internals.

Slightly more complex components

Now let's assume we have a more complex component, like a card:

<example-card heading="My heading">
  <p>My contents...</p>
</example-card>
Enter fullscreen mode Exit fullscreen mode

With a shadow DOM like so:

<div class="container">
  <div class="heading">
    ${this.heading}
  </div>
  <div class="content">
    <slot></slot>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

The heading could have been a slot here, but that isn't always the case, so let's assume it couldn't be this time.

If we wanted to allow the consumer to style the heading, we could quickly end up with some CSS like this:

.heading {
  margin: var(--example-card-heading-margin);
  color: var(--example-card-heading-colour);
  padding: var(--example-card-heading-padding);
}
Enter fullscreen mode Exit fullscreen mode

Of course, this becomes a bit of a pain.

The old way - @apply

One solution to this need for blocks of CSS rules already existed long ago when web components were still being finalised: the @apply rule.

It looked like this:

.heading {
  @apply --example-card-heading;
}
Enter fullscreen mode Exit fullscreen mode

Which was set like this:

example-card {
  --example-card-heading: {
    margin: .2em;
    padding: .2em;
    color: hotpink;
  }
}
Enter fullscreen mode Exit fullscreen mode

This tidied up those edge cases where people wanted to set several CSS properties at once, but it also opened the flood gates and made a bit of a mess (and lots of debates) so was ultimately deprecated and dropped.

The new way - ::part()

The modern solution, now accepted in all modern browsers, is ::part().

Similar to how slots work, an element is given a part and CSS rules can reference that part.

Our card from earlier would have a shadow DOM like so:

<div class="container">
  <div class="heading" part="heading">
    ${this.heading}
  </div>
  <div class="content" part="content">
    <slot></slot>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Which means you can style it via ::part():

example-card::part(heading) {
  padding: .2em;
  margin: .2em;
  color: hotpink;
}
Enter fullscreen mode Exit fullscreen mode

You can even use pseudo-selectors after this:

example-card::part(heading):hover {
Enter fullscreen mode Exit fullscreen mode

However, you cannot select a part within a part:

example-card::part(heading)::part(another-part) {
Enter fullscreen mode Exit fullscreen mode

Forwarding parts

If you have nested components, you may want to expose an inner component's parts via your own component.

Let's say you have a DOM tree like so:

<root-element>
  #shadow-root (open)
    <child-element>
      #shadow-root (open)
      <div part="childpart"></div>
    </child-element>
</root-element>
Enter fullscreen mode Exit fullscreen mode

If you want to expose childpart to consumers, you can use exportparts:

<child-element exportparts="childpart: rootpart">
Enter fullscreen mode Exit fullscreen mode

This means your root-element will be exposing a part named rootpart.

The syntax here is essentially inner-name: outer-name and is a comma-separated list so you can expose multiple parts.

Wrap up

This is all cool stuff, but you should still carefully consider whether your styling really should be exposed or not.

If a page has been architected well, I would imagine most of the components are fairly atomic and wouldn't need to expose so much of their styling to the consumer. If you make good use of slots and CSS variables, you probably shouldn't need parts so often.

Where it could be useful is if you had a complex, high level component like an app shell or a data grid. In these cases you may need to expose a lot of styling.

Top comments (0)