::part and ::theme, an ::explainer

(get it? :: ? I made a funny)

Shadow DOM is a spec that gives you DOM and style encapsulation. This is great for reusable web components, as it reduces the ability of these components’ styles getting accidentally stomped over (the old “I have a class called “button” and you have a class called “button”, now we both look busted” problem), but it adds a barrier for styling and theming these components deliberately.

Since a lot has changed since the last time I talked about styling the Shadow DOM, I wanted to give you a quick update about what new specs were in the works! Please note that this spec isn’t quite final, which means that a) the syntax and capabilities will likely change and b) there isn’t a polyfill you can use for realsies.


Ok, so. When talking about styling a component, there are usually two different problems you might want to solve:

💇 Styling: I am using a third-party <fancy-button> element on my site and I want this one to be blue

🎨 Theming: I am using many third-party elements on my site, and some of them have a <fancy-button>; I want all the <fancy-button>s to be blue.

Here’s almost everything I know on this topic.

A trip through time

There have been several previous attempts at solving this, some more successful than others. If you’ve read my last post about this, you’re already caught up. If you haven’t, here’s the deets:

And now: something different but the same

The current new proposal is ::part and ::theme, a set of pseudo-elements that allow you to style inside a shadow tree, from outside of that shadow tree. Unlike :shadow and /deep/, they don’t allow you to style arbitrary elements inside a shadow tree: they only allow you to style elements that an author has tagged as being eligible for styling. They’ve already gone through the CSS working group and were blessed, and were brought up at TPAC at a Web Components session, so we’re confident they’re both the right approach, and highly likely to be implemented as a spec by all browsers.

How ::part works

You can specify a “styleable” part on any element in your shadow tree:

<x-foo>
  #shadow-root
    <div part="some-box"><span>...</span></div>
    <input part="some-input">
    <div>...</div> /* not styleable
</x-foo>

If you’re in a document that has an <x-foo> in it, then you can style those parts with:

x-foo::part(some-box) { ... }

You can use other pseudo elements or selectors (that were not explicitly exposed as shadow parts), so both of these work:

x-foo::part(some-box):hover { ... }
x-foo::part(some-input)::placeholder { ... }

You cannot select inside of those parts, so this doesn’t work:

x-foo::part(some-box) span { ... } nor
x-foo::part(some-box)::part(some-other-thing) { ... }

You cannot style this part more than one level up if you don’t forward it. So without any extra work, if you have an element that contains an x-foo like this:

<x-bar>
  #shadow-root
    <x-foo></x-foo>
</x-bar>

You cannot select and style the x-foo’s part like this:

x-bar::part(some-box) { ... }

Forwarding parts

You can explicitly forward a child’s part to be styleable outside of the parent’s shadow tree. So in the previous example, to allow the some-box part to be styleable by x-bar’s parent, it would have to be exposed:

<x-bar>
  #shadow-root
    <x-foo part="* => bar-*"></x-foo>
</x-bar>

The ::part forwarding syntax has options a-plenty. 🙏 Feel free to skip these if you’re not interested in the minutiae of the syntax!

The “all buttons in this app should be blue” 🎨 theming problem

Given the above prefixing rules, to style all inputs in a document at once, you need to Ensure that all elements correctly forward their parts and Select all their parts.

So given this shadow tree:

<submit-form>
  #shadow-root
    <x-form part="some-input => some-input, some-box => some-box">
      #shadow-root
        <x-bar part="some-input => some-input, some-box => some-box">
          #shadow-root
            <x-foo part="some-input => some-input, some-box => some-box"></x-foo>
        </x-bar>
    </x-form>
</submit-form>

<x-form></x-form>
<x-bar></x-bar>

You can style all the inputs with:

:root::part(some-input) { ... }

👉 This is a lot of effort on the element author, but easy on the theme user.

If you hadn’t forwarded them with the same name and some-input was used at every level of the app (the non contrived example is just an <a> tag that’s used in many shadow roots), then you’d have to write:

:root::part(form-bar-foo-some-input),
:root::part(bar-foo-some-input,
:root::part(foo-some-input),
:root::part(some-input) { ... }

👉 This is a lot of effort on the theme user, but easy on the element author.

Both of these examples show that if an element author forgot to forward a part, then the app can’t be themed correctly.

How ::theme works

::theme matches any parts with that name, anywhere in the document. This means that if you hadn’t forwarded any parts, i.e.:

<x-bar>
  #shadow-root
    <x-foo></x-foo>
    <x-foo></x-foo>
    <x-foo></x-foo>
</x-bar>

You could style all of the inputs in x-bar with:

x-bar::theme(some-input) { ... }

This can go arbitrarily deep in the shadow tree. So, no matter how deeply nested they are, you could style all the inputs with part="some-input" in the app with:

:root::theme(some-input) { ... }   

Demo

As mentioned before, this spec is still in the works and we don’t have a shim that you can use in production. Hell, this shim isn’t even guaranteed to work for all the cases that should work according to the spec, so you should take this code with an enormous iceberg of salt. This is a demo that illustrates styling and theming a bunch of vanilla custom elements in a form.

Some notes: