Posterous theme by Cory Watilo

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.

HTML Component Model & the Shadow DOM

It’s that time of the year, the time when one’s brain, stomach and liver are pushed beyond their operating limits. Yep, it’s conference season.

Last week I went to Fronteers and GOTO Aarhus. Both were great, but it’ll take something incredible to stop Fronteers being my favourite conference of the year.

Alex Russell was speaking at both, and covered (among other things) scoped styling and the HTML component model. Now, I don’t really care for scoped styling (see my mini rant on twitter), but the HTML component model has really caught my imagination.

Defining away magic & giving developers creativity

Allow me to slightly abuse a quote from Storm by Tim Minchin

Throughout history every mystery ever solved has turned out to be not magic

As platforms develop they tend to create magic as a side effect, then later the magic is removed and turned into toys. I’m serious, stop looking at me like that.

  • This is a list item…

Back in the dark days, a list item would have a margin and a bullet point, this was created by the browser & there wasn’t a lot you could do about it. This was a problem if you wanted to express something as a list but didn’t want it to look like the browser wanted it to. The styling was magic.

Along came CSS and explained why elements look like they do, and gave us a brand new toy box, we could change the style of elements. Today we can inspect a list item & see the default styling a browser has given to that element, along with how our own styles override those defaults. Less magic! More toys!

There are still bits of magic hanging around that give us trouble, the <legend> element springs to mind, which I’ve avoided using even though it was a ‘best-fit’, because it’s too much of a cross-browser time-vampire to style.

Some elements have a lot of magic going on:

Audio element

This is an <audio> element, or at least its implementation in Google Chrome. As you can see it has buttons with behaviours & a slider control. These are implemented as DOM elements in Chrome (and other browsers), but they have a magic invisibility cloak. We can’t access the buttons or slider control using document.querySelector or similar, they’re not visible in Web Inspector / Firebug. Also, they have a magic style forcefield:

audio * {
    border: 3px solid red;
}

The above has no effect on the controls of the audio element. Magic!

Soon it won’t be magic, it’ll be toys.

HTML Component Model

Our saviour in this case, is the HTML Component Model spec. It’s very much a draft, there are no implementations in current browsers, and it may completely change by the time I’ve finished writing this sentence… thankfully it hasn’t yet… still the same… ok I think it’s safe to continue.

The component model turns the following bits of magic into toys:

  • How a particular tag name is linked to a constructor and prototype
  • How a single DOM-visible element appears to be made up of multiple parts
  • How those parts are protected from some/all styles

Linking tags to constructors

Here’s how I’d create a modal dialog element:

// Creating a constructor
function ModalDialog() {
    // We're inheriting from HTMLElement, so call its constructor
    HTMLElement.call( this );
}

// Inherit from HTMLElement
ModalDialog.prototype = Object.create( HTMLElement.prototype );

// Give the element a behaviour
ModalDialog.prototype.helloWorld = function() {
    console.log( 'Hello world' );
};

// Apply it to this document
Element.register( 'x-modal-dialog', ModalDialog );

Once the element is registered, any <x-modal-dialog> elements will be reinitialised as instances of ModalDialog.

// Logs 'Hello world', if there's a <x-modal-dialog> in the document
document.querySelector( 'x-modal-dialog' ).helloWorld();

However, something like a modal dialog wouldn’t typically exist on the page already, it would be added via js. To add a new ModalDialog:

// Old familiar factory method
document.body.appendChild( document.createElement('x-modal-dialog') );

// "Why didn't we have this before?" constructor method
document.body.appendChild( new ModalDialog );

// From a string of markup
document.body.innerHTML = '<x-modal-dialog>';

I’m unsure if the ‘x-’ prefix will be enforced, or simply convention.

The shadow DOM

The shadow DOM is seriously cool. The regular DOM is clean-shaven, does charity work & goes to bed early on a school night. The shadow DOM has a goatee, sits in a bar and tells you to go fuck yourself if you ask it for an autograph.

The shadow DOM is where you add elements that make up your component. In the <audio> example, the shadow DOM contains the buttons and the sliders

function ModalDialog() {
    HTMLElement.call( this );

    // Make a close button
    var closeBtn = new HTMLButtonElement;
    closeBtn.innerHTML = 'Close';

    // Give our dialog a shadow DOM
    var shadow = new ShadowRoot( this );

    // Add the button to the shadow DOM
    shadow.appendChild( closeBtn );
}

Now our dialog has a close button which doesn’t appear in document.querySelectorAll('button'). Our close button doesn’t actually do anything when it’s clicked, it’s just a button, we’d add that behaviour with js.

The ShadowRoot is a similar to a document fragment, in that it acts more like a collection of elements than an element itself.

Adding children to your element

So far our component behaves like <img> and <input>, as in it cannot have child nodes. This is pretty silly for a dialog, as you’d expect to contain flow content, eg:

<x-modal-dialog>
    <h1>Keyboard not connected</h1>
    <p>Press F1 to continue</p>
</x-modal-dialog>

Making this work is pretty simple:

function ModalDialog() {
    HTMLElement.call( this );

    var closeBtn = new HTMLButtonElement;
    closeBtn.innerHTML = 'Close';

    var shadow = new ShadowRoot( this );
    shadow.appendChild( closeBtn );

    // Add a content element to the shadow DOM
    shadow.appendChild( new HTMLContentElement );
}

Done! Anything inside our <x-modal-dialog> tag will be moved to the <content> element within the shadow DOM, which appears after our close button. This means you can have multiple elements around <content> that build up the interface of your component.

Everything in its right place

Currently we’re dumping all the content into one content element. That’s cool, but some elements might need to appear in special places. We see this with elements like <thead>, even if they appear after the <tbody> in markup, they’ll render above it.

This was magic. Now we have toys:

function ModalDialog() {
    HTMLElement.call( this );

    var shadow = new ShadowRoot( this );

    // Make a header for our dialog & add it to shadow DOM
    var header = document.createElement( 'header' );
    shadow.appendChild( header );

    // Add a content element to the shadow DOM
    var bodyContent = new HTMLContentElement;
    shadow.appendChild( bodyContent );

    // Add another content element for header stuff
    var headerContent = new HTMLContentElement;
    header.appendChild( headerContent );

    // Set it to receive h1 elements
    headerContent.select = 'h1';
}

The select property can be a series of space-separated simple selectors. Very cool. Surprisingly this includes pseudo-class selectors, meaning you could make something switch to another location in the shadow DOM on :focus or :checked, interesting.

Modifying the shadow DOM of existing elements

An element can only have one shadow DOM, if one is created for an element that already has a shadow DOM an exception is thrown.

I’m not a huge fan of this, but Alex proposed a better solution over beers: An element can only have one shadow DOM, if another is created the existing shadow DOM is removed.

This would allow us to do this:

var select = document.querySelector( 'select[name="country"]' );
var shadow = new ShadowRoot( select );

Now we’re able to recreate the look and feel of the <select> by creating our own shadow DOM. As long as we manage properties like value, the browser will include that value when the form is submitted.

This solution caters for browsers that may not already use a shadow DOM for a particular element. I’m pretty certain most browsers use a shadow DOM for a select element, remember the fuss we had when IE didn’t. However, mobile browsers tend to bring up a native bit of UI when the select element is interacted with. This is a good thing, browser vendors should be free to go the native or shadow DOM route, whatever gives the best user experience. However, if I were to create a new shadow DOM for a <select>, the browser should hand full control of the element to me, and not use any native UI trickery on interaction.

Won’t someone think of the semantics!

The great thing about this article being so unexpectedly long, is anyone who disagrees with allowing developers to extend HTML has long since punched a wall and stormed out of the room muttering obscenities. So, y'know, fuck those guys.

Oh ok. Let’s talk semantics.

In the same way you wouldn’t use a <div> when <article> is a better fit, don’t make your own component if something already does the job. But, you want it to look a bit different, either change the shadow DOM for individual instances, or consider inheritance.

In the previous example I changed the shadow DOM for an individual select element. For multiple select elements, perhaps I could do this:

function StyledSelect() {
    // We're going to inherit directly from select...
    HTMLSelectElement.call( this );
    // Add a lovely looking shadow DOM here
}

// Take everything the select already has
StyledSelect.prototype = Object.create( HTMLSelectElement.prototype );

// Register it
Element.register( 'x-select', StyledSelect );

Now my <x-select> elements have the same semantics and interface as <select>, because it inherits from it. Its value property should be serialised when its parent form is submitted.

// Maybe even, I could do this...
Element.register( 'select', StyledSelect );

This is all guesswork and hoping, as I mentioned earlier I don’t know if the ‘x-’ prefix is convention or enforced. I’d quite like to be able to change the behaviour of all selects in one swoop. I think I would… or would it be the end of the world? Hmm.

If you’re creating something which has no resemblance to any existing element, then you’re not losing any semantics, because there were none to begin with. However, you should make sure your shadow DOM children are using elements that best-fit what they do, and use WAI-ARIA to fill in any gaps for assistive technologies.

Styling your components

Oh yes, I keep mentioning styling don’t I… I’ll save that stuff for another article.