Necessary context
Recently, I was listening to a well known podcast, and the awesome hosts were talking about web components, listing all of the good, bad, and ugly parts about the various web components specs and features.
Shoutout to Chris Coyier and Dave Rupert for the totally fair episode and great conversation. Give the episode a listen here:
Anywho, deep in that episode, I was surprised to find that CSS Parts made it onto the baddie list, though I can see some of the reasons behind why folks might think they're not the best. I happen to think that not only are CSS parts pretty good, they are actually saving all of us that use them from a scenario that is WAY more annoying and frustrating than not being able to style every single element inside a web component shadow root exactly like you want.
Let me explain, but first MORE context!
Web components were designed mostly for third party usage
I cannot explicitly verify all of the following to be true, but my personal opinion—created while writing, using and helping spec web component features for the last few years in my day job—is that the existing web component specs and features were all designed to solve the "third party component" use case. That is, web components were designed to support a very specific situation: a component author creating a tool that other consumers download from the internet and consume in their apps.
I feel that if you look at web component features through the lens of the "third party component" use case, then a lot of the approach that was taken by spec writers and browser implementers make a TON of sense. Take the severity of shadow dom style encapsulation for example. If you are pulling a component off the internet and into your app, isn't it wonderful to not have to worry that that component's css is going to somehow break one of your pages? Isn't it wonderful to not have to concern yourself with the internal structure of a component you didnt write and get to treat that component like a black box and interact with it through a well-defined api surface?
Most of the trouble with understanding how web components are designed to work is that the industry has moved on since the web component specs were created and these days 99% of the components we use in our apps don't come from external sources. These days, when we think of components, most of the time we're thinking about a "first-party component" that we or our team wrote and not some person across the internet. We don't use that many external components in our applications, so web component APIs designed mostly for that use case seem strange to us, especially when we also try to use web components to create first party components for ourselves.
So for everything else I'm going to say in this article, assume I'm referring to the "third-party component" situation. Imagine that the component I'm talking about is an npm package that you downloaded and that there's a GitHub readme out there and patch notes and such, but you (or your team) didn't write it yourself for your own app.
CSS Parts are not a great interface for styling components that you write for yourself or your team and have full control over.
CSS Parts are part of the public API of a web component
Well-designed components have several different ways to interact with them. Great web components have slots for customized HTML, CSS custom properties for customized theming, and CSS Parts for customized styles. The Shop Talk Show fellas discussed CSS Parts with the context being that they are clunky for free form styling, but I wouldn't think about CSS Parts that way. I'd think about them as another public API surface for the component, much the same way as attributes or properties. Folks usually don't worry too much about not being able to create custom properties or attributes for components they pull from the internet. I think CSS Parts are the same idea. The component author designates the pieces of the internal template that are "ok to style" any way you want. So being able to style every single internal element in that shadow dom web component shouldn't necessarily be an expectation.
And who better to know which parts of some complex template are ok to have any style applied than the component author? If you pull in a component from the internet just to turn around and re-write all the styles, possibly destroying accessibility and such in the process, why did you install the component in the first place? I get the attractiveness of the "I know what I'm doing" selector for shadow roots that lots of folks have advocated for, but in this next section, I'm going to illuminate why I think having a defined styling API surface that is disconnected from the internal DOM structure is actually amazing and should be celebrated for helping all us devs not go crazy.
Cue old-school movie trailer voice
In a world where the "I know what I'm doing" selector exists and CSS Parts don't
Lets jump into an imaginary world where CSS Parts don't exist, and some "I know what I'm doing" selector (let's call it /deep/
for the sake of nostalgia) exists and is "the way" that you are supposed to style the internal DOM template of some shadow root web component.
And you need one such component for your app, so you go and download an npm package
npm i some-awesome-package@1.2.3
and have the following HTML in your application:
<some-awesome-component></some-awesome-component>
The Some Awesome Component
component has a div in it that is red by default and doesn't use any CSS custom properties. And at first, the red div is perfectly ok!
But then you decide that you need to change the color of the red div to rebeccapurple
. So you write this css:
@layer my-overides {
some-awesome-component /deep/ div.the-red-div {
background: rebeccapurple;
}
}
the-red-div
is a terrible name for a class, but hey, no one cares about the internal structure of a shadow dom web component, right? They can't conflict with the outside world, so component authors can write terrible class names if they want to.
And because we're in our imaginary world where /deep/
exists, everything works! The red div is now purple and everything is coolio.
You ignore the tiny cognitive dissonance in our brains that
.the-red-div
has a color that isn't red. That's a tomorrow problem.
Sweet!
Then the component author adds a feature and publishes a patch bump.
You're busy shipping and along the way, you re-installed an unrelated package and you pulled the latest patch bump of Some Awesome Component and the red div is back again!
What gives? You go searching the patch notes and you find:
## 1.2.4
- [a987s83] Added cool button, fixed a11y issues
Seems fine, why did the css break? Changelog notes don't have any clues. So you skim the latest few PRs and don't see anything that jumps out at you.
So you spin up your application and go checking the shadow root and the red div is a <span>
now! And the component author has realized that their class name of the-red-div
should have been more generic, and has changed it to colorful
.
Ok fine, so you edit your css to:
@layer my-overides {
some-awesome-component /deep/ .colorful {
background: rebeccapurple;
}
}
And it works fine again!
Then the author releases another patch bump next week.
Jump cut back to the real world
Direct CSS selector access to DOM you don't control is a huge footgun
See the pattern and therefore problem with direct selector access in to internal templates that you don't own or control?
Inevitably, changes will be made to the internal template outside of the control of you, the component consumer. And inevitably those change will happen in a way that is almost invisible to you without visually inspecting EVERY SINGLE PIECE of your running app, either manually or with flaky visual regression testing tools. If you are using a component from the internet, then there is a contract of sorts. The component author owns the shadow dom, and you the consumer own the :host
element (the <some-awesome-component>
HTML tag itself in your code that you write). Crossing that boundary without some sort of contract in place is a guarantee for brittle code.
Fun fact, this brittleness also happens when doing direct shadow dom querySelectors for DOM elements in JS too. If your app depends on some element to always exist, it won't. If you query into DOM you don't control, one day some element won't be there. Either it will be a different element or have a different class/attribute/id on it.
But CSS Parts have your back
Because CSS Parts are simple strings that are not DOM elements themselves, using them shields your application from brittle code. Third party authors are never going to test that some div is ALWAYS a div forever. But if they create a CSS Part, they know that they are creating a public API for the component that can't be removed or changed without a breaking change. And if you use CSS Parts instead of directly querying shadow DOM, then the component author can move that CSS Part around in the shadow dom and your application code will not break.
Furthermore, because CSS Part names imply relationships, concepts, and features, it would be hard for a component author to refactor a component in such a way as to make the CSS Part change its meaning substantially. So in addition to knowing that your CSS Part using code isn't going to break as the component author makes changes without telling you, you can also be reasonably sure that the CSS Part will always be the same kind of thing in relationship to the overall component. So the styles you apply to that part will be reasonably sure to always "make sense" for that part even as the component evolves over time.
One step further
I've seen a few comments from very smart and valuable industry folks about the pain of CSS Parts and that adding direct shadow dom access via selectors would be much more convenient. If nothing every changed shape over time I would wholeheartedly agree. But since components do change over time, I would venture to suppose that direct shadow DOM access will be more painful than having to remember which CSS Parts are available for use.
Whenever there are discussions about how to cross the boundary between "the app" and the shadow dom inside a component, I would always advocate for some named structural contractual approach like CSS Parts over direct selector access. In my view, anyone that thinks that CSS Parts are clunky and terrible has just not had to go hunting through their entire app looking for all the places a shadow root template has changed without their knowledge :)
Conclusion
CSS Parts are great and when you use them, even though you 100% are a great developer and you know what you are doing, you 100% also don't want the pain of potentially having to tweak all of that custom CSS you wrote every single time there's a patch bump to components you use. You definitely know what you're doing, but all of us want to prevent our apps from randomly breaking and thats what direct shadow DOM styling will do. An "I know what I'm doing" selector will definitely get your styling to work, but it will only be guaranteed to work if you literally never update the package version you styled. If you do update the version you'll have to keep an eye on all of those "I know what I'm doing" styles every single time.
Imo thats a waste of our precious time. just use CSS Parts and encourage component devs out there to add them to the the components they write.
Top comments (1)
I agree completely! Accessing the shadow DOM via CSS on third-party components is something that will almost surely cause you headaches in the future. For me, it feels like trying to access the private properties of a class.
Fantastic article!