Scoped styles & the Shadow DOM
Wow, this blog now contains double the number of posts of any other I’ve tried to maintain.
A few weeks ago I got a bit excited about the HTML Component Model, and how you could use it to create elements that have a “Shadow DOM” which couldn’t be accessed by the DOM methods we currently use. I also pointed out that these elements weren’t affected by style rules on the page, in the same way the controls of an <audio> element can’t be changed using regular CSS.
So, how can we add styles to our shadow DOM? Actually, that probably deserves its own heading…
How can we add styles to our shadow DOM?
function ModalDialog() { HTMLElement.call( this ); // Make a close button var closeBtn = new HTMLButtonElement; closeBtn.innerHTML = 'Close'; // Give our dialog a shadow DOM this.shadow = new ShadowRoot( this ); // Add the button to the shadow DOM this.shadow.appendChild( closeBtn ); addModalDialogStyle( this.shadow ); } // Apply it to this document Element.register( 'x-modal-dialog', ModalDialog );
The above should be familiar if you read my previous post. I’m setting the shadow to this.shadow, this isn’t required, but was recommended as a convention by Alex Russell in the comments. So, what happens in addModalDialogStyle? There are a few suggestions, one is to use scoped styles:
function addModalDialogStyle( shadow ) { // Being able to use constructors like this is ace var style = new HTMLStyleElement; // Set the contents style.innerHTML = 'button { background: green }'; // Ohh, new toy... style.scoped = true; // Add it to our shadow shadow.appendChild( style ); }
The scoped attribute & property is defined by the whatwg, it means a style block will only apply to its parent element, and everything within its parent element.
In most cases you’d use @import to pull in a stylesheet from a .css file, but in this case I’ve added one simple rule. Every button in our ModalDialog shadow DOM will have a green background, but will have no effect on buttons the rest of the document.
I think this is pretty useless
I’m not a fan of this approach, I don’t think we need the scoped attribute. Why?
Backwards compatibility issues
At time of writing, no browser supports scoped. If you try to use it, the rules will apply to the whole document. This is a pretty shitty adoption path.
Ever tried to use selectors like .foo.bar in IE6? They don’t work, IE6 treats it as .bar. This catches developers out because it doesn’t outright fail, in fact it appears to work in basic tests. I think we’ll see the same mistakes with scoped.
However, I acknowledge this won’t be an issue for shaddow DOM styling if all browsers that support ShadowRoot also support scoped.
Performance issues
It’d be pretty stupid to argue about performance on an unimplemented feature. But I’m feeling pretty stupid right now, so let’s go…
If your scoped styles are in a separate CSS file, you’re adding another HTTP request. One for each different component you use (multiple uses of the same component won’t trigger additional requests), they can’t be combined the same way JS and unscoped CSS files can.
If you’re not using @import then each instance of your component is going to introduce another stylesheet to parse, and another CSSOM to build and retain in memory. So if you have 100 instances of your component on the page, your component’s stylesheet is going to be parsed and retained in memory 100 times. The browser could string-match the styles and try to retain one instance in memory, but they are unique objects, you’d be able to alter them independently via the CSSOM, the browser would have to branch them in memory in this case.
Does it really give us anything useful?
p { /* Applies to all paragraphs */ } x-modal-dialog p { /* Applies to all paragraphs, scoped to our ModalDialog component */ }
The above is how it already works. We already have scoped styles in CSS through the descendant selector (and child selector etc etc), and that lets us serve a single CSS file for our page. That single CSS file creates one CSSOM.
While we’re on the subject of specificity, there’s nothing in the spec about the impact scoped has. I assume none, meaning styles with selector html p in the global stylesheet will overwrite those with selector p in a scoped stylesheet.
The scoped attribute on style elements is a pretty shitty solution to a problem we don’t have.
Let CSS solve the CSS problem
I think we should just do this to ‘scope’ styles:
x-modal-dialog p { /* Applies to all paragraphs, scoped to our ModalDialog component */ } /* Or even better... */ x-modal-dialog { & p { /* Applies to all paragraphs, scoped to our ModalDialog component */ } }
The second example makes use of selector hierarchies, which will burst out of the W3’s doors with pomp and ceremony, to be greeted by a slow clap from the developer community. (Edit: Gah, that does make me sound like a dick. Really appreciate the work that’s going into that spec, just irritated that it’s taken the powers-that-be so long to accept it’s something we need.)
However, the rule above will only target paragraphs inside x-modal-dialog, it won’t touch paragraphs inside its shadow DOM. They’re protected.
I thought this whole thing was about styling the shadow DOM?
Hmm, yes, good point. The truth is that part of the spec is very much in the brainstorming phase.
Styling for the author of the component
Component author styles rely heavily on scoped styles inside the shadow root at the moment. This is unnecessary, as CSS already recognises shadow DOM elements in the spec, except it calls them ‘pseudo-elements’.
input::placeholder { color: green; }
Here we’re assigning styles to an element within the <input>’s shadow DOM. All we need is something like that, but accesses the an element’s ShadowRoot:
x-modal-dialog::shadow { & button { padding: 10px; } & content { margin: 0 10px; } }
Here we’re styling the <button> and <content> elements within our shadow DOM. This way the styles for our component can be combined and minified along with the rest of the styles for the site.
Styling for the user of the component
A user of a component could attempt to style it using ::shadow, but they shouldn’t. The fact that they can may be seen as a weakness, but it’s not creating a new problem, we’re used to being able to mess with stuff that we probably shouldn’t in most cases. For example, in JS we can override the constructor for Object and break pretty much all scripts running on the page. We’re used to this kind of freedom. We shouldn’t try to be like Java, the web isn’t Java, I know this because my keyboard isn’t swimming in tears.
XBL lets component authors define pseudo IDs, so if an element had a pseudo ID of ‘foobar’, it could be selected via component::foobar.
Letting component authors specify their own pseudo IDs is a no-go, as it means the W3 can’t add any more without risking naming collisions. But we already have the x- convention as a solution to this. So the user could write:
x-modal-dialog::x-close-btn { color: #fab; }
As component authors, we’d accomplish this by giving our button a pseudo ID of x-close-btn, probably via an attribute.
Yey for components
My excitement about components is probably a bit premature, and a lot of what I’ve written here is purely conjecture (premature conjectulation, anyone?).
JavaScript UI components are really fragile at the moment due to how other bits of JS or CSS can unintentionally interfere with them. JS components tend to be built with <div>s rather than more semantic elements, purely because <div>s are less likely to have a redefined style. Components solve this problem, we get better semantics and stuff that’s just easier to use.
Looking forward to seeing how this spec evolves.