I think the styling story for Shadow DOM Web Components is not great. I’ve got what seems to me like a simple idea that would help that.
Fair warning: I’m just some dude on the internet with an opinion here. I’m coming at it from an author’s perspective who has only written a handful of web components. But I like the idea of them, I like the web platform, and I’ve been trying to watch them for what feels like a long time. I don’t have the wider perspectives of those who have been in the trenches, speccing and implementing these things.
Let me get right to it. What I mean is using a pseudo selector to break through the Shadow DOM and then be able to style inside it, from any CSS on the page:
my-component::i-know-what-im-doing {
.something-inside {
/* yay, we're in! */
}
}
Code language: CSS (css)
Now that selector is just tongue-in-cheek (reminds me of React’s dangerouslySetInnerHTML
), but the idea I’m serious about. Maybe it’s something more straightforward like:
my-component::root {
.something-inside {
/* yay, we're in! */
}
}
my-component::root footer h3 {
/* When you're in, select and style like normal CSS */
}
Code language: CSS (css)
This is not possible right now, and I wish it were.
Current Styling Options of Shadow DOM Web Components and Why I Don’t Like Them
I’ll elaborate on the thoughts I posted in another article about Web Components. Here are what seem to be the current possibilities for styling a Shadow DOM Web Component:
- Styling specific elements with
::part
is a very niche styling approach that is not a real styling solution. It tosses out the power of CSS (selectors) which seems silly. - Styling by documenting somewhere that certain CSS
--custom-properties
are in use, thus you can set them from the outside, and they will cascade in to affect inner styling. This is also limited, not offering the real power of CSS, and to me not a real styling solution. - Styling by injecting a
<style>
tag into some template literal in the JavaScript itself. This feels awkward and ad hoc to me. Writing CSS as a string is not great (you’d be lucky to get autocomplete, lining, etc.) I’m not a fan. - Styling with an adopted stylesheet means an additional web request for each component or back to using a string which is either awkward or slow.
I don’t want to do any of those. They all force me out of the styling workflow of the rest of my website, and tend to push me towards nope, screw it decisions.
The Advantages of This Approach
- You still get the encapsulation. For the most part, anyway. The JavaScript encapsulation stays the same. Even CSS encapsulation is still there. Random CSS will not leak in. You have to be very specific, making the component name part of the selector and using the specific pseudo-selector to reach into these components. That’s not accidentally done.
- You can now style things with the best styling API around: CSS.
- You can concatenate/bundle the styles for your site how you already are doing it. You just… style stuff.
- You can now choose to create Shadow DOM Web Components, and get the excellent feature of
<slot>
s, without being penalized with difficult styling choices. - All the other styling methods are still there.
- It would allow people to build and distribute a Shadow DOM Web Component with very basic styling and they could just tell people: hey you wanna style it? Go for it.
OK bye.
Interesting thought from Nolan:
I guess I’m saying: the component consumer.
A hardline stance on this would be that I don’t care about component authors that don’t want me to do this. It’s my website. I want control.
But I could imagine a softer stance where there is some kind of (new) method for force-not-allowing outside styles.
Beyond changing the distributed component code I do not believe web components easily allow mode switching. This is purposeful because the nested children may not be rendered or used in the same way as the plain html shows.
For example a list component may just have a single ul in the dom but jump across the shadow boundary and that list could be rendered inside a data tables setup with some other magic sauce added. This could work on the regular dom but leveraging the shadow dom allows non-destructive updates. (Changing the list without updating the original or the reverse. Data manipulation that updates the existing dom list)
Web component styling is still a giant pain for no reason though. There are work arounds but the encapsulation is too aggressive. It causes more pain than what it solves.
Sound like
::nuke
is a better name for this selector.People who know what it does will use it properly; the other 80% of developers will go WTF? I didn’t hit the red button
I don’t understand.
Piercing the shadow dom with CSS will defeat the idea of shadow root encapsulation. It was designed this way, use ::part
Just not a fan of
::part
. It’s fake CSS. You can’t do::part > h3
. You can’t do::part:checked
. You have to document what has a part and what doesn’t so any consumer can find it. It’s not a real styling solution.Not knowing what does and doesn’t have a part is huge. In React, you can use normal CSS to target anything normally. Why not with Web Components too?
We really need this. I like the idea of scoping it based on the custom element name. It could also not penetrate deeper without further specifying the element-name::root to further penetrate into. Examples:
Etc.
Regarding the current methods, with the last two options, we don’t have to write a string. We can import from a
.css
file, making it compatible with CSS tooling:Note that a
<link>
element with href pointing to a CSS file is another way to add a style without using a string for CSS.shadowRoot.appemd(linkElement)
.Another trick to making elements widely stylable is accepting a stylesheet attribute and equivalent JS property. This is what I’ve done with the
<code-mirror>
element (for controlling a CodeMirror text editor instance, https://github.com/lume/code-mirror-el) to allow styling its content in any way.Here’s the HTML usage:
The JS property also does similar, and accepts various types of input:
JS usage:
or
The implementation of
.stylesheet
is here:https://github.com/lume/code-mirror-el/blob/4bbf21b111a38805107a58fd8fac78a47160eee7/src/CodeMirror.ts#L55-L68
That JS property receives a string from the HTML attribute, or the differing types of values directly via JS.
The fully dynamic nature of the attribute/property is here where it simply either appends the style/link element to the shadowRoot, or it pushes to adoptedStyleSheets:
https://github.com/lume/code-mirror-el/blob/4bbf21b111a38805107a58fd8fac78a47160eee7/src/CodeMirror.ts#L165-L180
If we change the value of
stylesheet
, it will update as expected. (It uses Solid.js reactivity underneath, https://solidjs.com)Perhaps I can make it also accept an array of the aforementioned types of values, so that multiple styles can be passed in, f.e. from multiple files.
Another idea is to release that
stylesheet
feature (maybe renamed to stylesheets if supporting an array) as a standalone mixing usable on any custom elements.Maybe another name could be
styles
instead ofstylesheets
.Lume Element is a library for making custom elements, and is an alternative to other libraries like Lit.
https://github.com/lume/element
There’s a web components styling framework here which can do everything you mention, but in different approach
https://keenlycode.github.io/adapter/
I’m the author and would be happy if this one can solve CSS issue for developers 😉
Hey Um! Although that’s a great workaround for now, the ideal native solution would not require any JavaScript, only HTML and CSS.
There was the /deep selector in the shadowDom V0, see https://stackoverflow.com/questions/25609678/what-do-deep-and-shadow-mean-in-a-css-selector
I remember this! I think we need something similar, but with more control like the idea here.
I fixed so many awful code during the years, I am sure people who wrote it were all convinced they knew what they were doing
With great power comes great responsibility. 🙂
I 100% get the desire for this, but shadow DOM right now is offering a promise to authors that they can lock users out of styling their web components. I could imagine people being pretty sour if they had built web components using shadow DOM explicitly for this reason and then having that guarantee taken away. Unfortunately, I feel like this would go against the “don’t break the web” ethos — wouldn’t it?
In any case, hopefully people start using more light DOM with web components anyways, obviating the need for something like this.
So basically the reintroduction of ::shadow or /deep/ selectors.
These solutions were removed because they felt way too powerful (and possibly performance? I don’t recall exactly..)
Right now it is designed to give component authors control. My take on that is if a component consumer is desperate enough they will always find a way, it’s what programmers do. I prefer keeping things flexible and it feels like the main thing holding webcomponents back is not being able to style their shadow DOMs. CSS frameworks or just using external global stylesheets (think Tailwind) also just isn’t compatible with the idea and design of shadow DOM, I think Justin Fagnani’s open stylable proposal tackles that use case as well whereas a penetrating shadow selector does not, so I feel like that idea should be considered too.. still opt-in for component authors but perhaps should have a “danger hatch” for consumers to make it open?