One of the beefs (and there arenât many) that I have with CSS is that it has a very weak opinion about style encapsulation. That opinion is basically âwell, name your classes wellâ or else bad things happen. Know this: I come from C++, land of rules and disappointed compilers; this hand waviness drives me crazy.
This matters because now you have to trust the people that write your css libraries
to have common sense. If my website needs two kinds of fancy buttons, which live in shiny-button.css
and bouncy-button.css
, which are both libraries
written by silly people who want me to use the .button
class to get their style,
Iâm hosed.
Enter the Shadow DOM
The Shadow DOM fixes this problem by building a little castle (a dome, get it?) around each custom element, locking in its implementation and styles. This is a proper castle, with a proper moat, so now styles canât get in and out of it. This means that if <shiny-button>
was a custom element instead of a pile of CSS, its .button
class was scoped to the element itself, and wouldnât stomp over <bouncy-button>
âs similarly creatively named .button
class.
This shouldnât surprise you too much, as native elements have been doing this in secret for yeaaaaars. <input type=date>
styles the date picker somehow, but youâve never worried what class names it might use to do so. You know why? Because you canât get to its castle, thatâs why.
The struggle is real
So what happens if you do want to style <shiny-button>
? What if itâs a perfectly
respectable button, but it uses Helvetica as its font and you really need it to be Comic Sans because Helvetica is so 2014?
You can always style the host of the element. Think of the host as the castle walls; itâs the thing that holds all the actual contents of the custom element. It still plays by CSS rules, so some of the styles you set on the host could actually trickle down to some child elements. For example:
shiny-button {
color: white;
background-color: tomato;
border-radius: 3px;
width: 400px;
/* this will apply to any text in the button,
* unless a specific child overrides it */
font-size: 14px;
}
What you donât get to do is peek at the implementation of the <shiny-button>
and decide you donât need one of the nested
div
s it uses. Again, these are the same rules that <input type=date>
plays by: you can change the input
âs text to be red, but that date picker is what it is (hella ugly).
When the Shadow DOM was first introduced, people anticipated this styling problem and took the âbring an AK-47 to a knife fightâ approach by giving every developer dragons. These dragons are called /deep/
and ::shadow
, and let you cross the moat and tear the shit out of any castle. You
could style anything you wanted in your custom element, because ainât nobody stopping
dragons. Itâs like that moat isnât even there:
shiny-button /deep/ fancy-div.fancy-class > .button {
color: red;
}
However, as we know from Game of Thrones, you eventually discover that if you have a dragon, itâs going to start eating all your goats and people will regret giving you a dragon.
So we deprecated /deep/
and ::shadow
and web developers around the world panicked.
Bridges instead of dragons
The correct answer to âsay, how do I cross this moat?â isnât âlol a dragonâ. Itâs a bridge. Weâve been using bridges to cross waters for like 3000 years. Dragons arenât even real, man.
CSS variables (aka custom properties) do exactly that. Theyâre hooks that the developer of a <shiny-button>
has left all over the code,
so that you can change that particular style. Now you, as the user of a custom element no
longer need to know how that element is implemented. You are given the list of things you can style, and youâre set.
The code examples use Polymer, which is what I work on, and what I use to write custom elements. The full code, if you want to play along, is here (thereâs an embedded JSBin at the bottom of this post, but you know, spoilers).
First, a shiny button
So, hereâs our button. It has a bunch of nested silly things, because why not. Who knows how the native <input>
actually looks like. Maybe itâs divs
all the way down. Maybe itâs spiders. Itâs probably spiders.
Everything inside .container
, including .container
itself is inside the Shadow Castle, so it canât be reached:
<dom-module id="shiny-button">
<template>
<style>
:host { display: inline-block; color: white;}
.container { background-color: cornflowerblue; border-radius: 10px; }
.icon { font-size: 20px; }
.text-in-the-shadow-dom { font-weight: 900; }
</style>
<div class="container">
<span class="icon">âĄ</span>
<span class="user-text"><content></content></span>
<span class="text-in-the-shadow-dom">!!!</span>
</div>
</template>
<script>
Polymer({ is: 'shiny-button' });
</script>
</dom-module>
...
<!-- somewhere in an index.html, you'd use it like so: -->
<shiny-button>hallo hai</shiny-button>
The <shiny-button
> looks like the thing on the left. Pretty meh. Weâll do better. Weâll style it
to be the thing on the right, without any đ˛đ˛đ˛.
What can you style right now?
We can only style the host of the element â this is everything outside the .container
class, but inside
the shiny button. You know, the walls of the castle.
shiny-button.fancy {
font-family: "Lato";
font-weight: 300;
color: black;
}
To see the difference between the host and the container, we can give the button itself a different
background than the .container
. The red corners you see are part of the host; the blue parts are
the .container
.
Of course, none of these styles will work, because these divs
are well inside the castle:
shiny-button.fancy .container {
color: red;
background-color: pink;
}
shiny-button.fancy .text-in-the-shadow-dom {
font-weight: 300;
}
And now: some bridges
We probably want to change the buttonâs background color, so weâll create a variable for it, called --shiny-button-background
. Some things:
- every Polymer custom property needs to start with a
--
, so that Polymer knows youâre not just typing gibberish. - I like to include the element name as a prefix to the custom property; I find it useful to remind me what Iâm actually styling.
- I also like documenting these somewhere in a giant docs blurb, so that the elementâs users know what to expect. Polymerâs paper-checkbox is a nice example of this (because I wrote it, obvs).
Now that we know a custom property is available, this is how we would use it, inside the custom element:
.container {
/* cornflowerblue is a default colour, in case the user doesn't
* provide one. You could omit it if it's being inherited from above */
background-color: var(--shiny-button-background, cornflowerblue);
}
You can think of var
like an eval, which says âapply the value of this custom property, whatever that value isâ. And this is how you, the user of the element would actually give it a value:
shiny-button.fancy {
/* see how much this looks like a normal css property? i.e.
background: #E91E63; */
--shiny-button-background: #E91E63;
}
You can add all sorts of hooks for these kinds of âone-offâ custom properties. Eventually you will realize that if the thing that should be styled is too generic (the background container of the button) thereâs waaaaay too many CSS properties to expose one by one. In that case, you can use a mixin, which is like a bag of properties that should all be applied at once. By default this bag is empty, so nothing gets applied when defining the custom element:
.icon {
font-size: 20px;
@apply(--shiny-button-icon);
}
But the user of the element could start adding things to the bag like this:
shiny-button.fancy {
font-family: "Lato";
font-weight: 300;
color: black;
--shiny-button-background: #E91E63;
/* this is the mixin! the colon and the semicolon are both important */
--shiny-button-icon: {
color: red;
padding: 10px;
text-shadow: 0 1px 1px #880E4F;
};
}
Some tips:
- the mixin is only relevant to the selector itâs being applied to (modulo CSS inheritance rules). As an element author
itâs your responsability to name this mixin in a way that conveys this. In the example above,
--shiny-button-icon
implies youâre styling the icon of the button. If instead youâre applying that style to the text, for example, youâre being a bad element author, and your users will shame you on social media. - mixins arenât a panacea. If you look at the paper-checkbox example I mentioned before, youâll notice no mixins at all! This is because the element is fairly restricting, and thereâs only so many things you can possibly care about styling. Thatâs when I tend to prefer individual custom properties vs a mixin.
Thatâs it, thatâs all! We can style ALL the things now, AND get style encapsulation, and not sacrifice any goats to dragons. Arenât web components amazing? (Yes they are).
Hereâs the JSBin if you want to play with it: JS Bin on jsbin.com
Hear me talk about this
I gave this talk at the Polymer summit. Hurray, the metaphor is spreading!