| layout | default | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| type | core | ||||||||||
| navgroup | docs | ||||||||||
| shortname | Articles | ||||||||||
| title | A Guide to Styling Elements | ||||||||||
| article |
|
||||||||||
| tags |
|
{% include authorship.html %}
{% include toc.html %}
This article covers many of the new CSS rules, properties, and concepts for styling Custom Elements. While much of it is applicable to general Web Components, it specifically focuses on:
- How to use these new CSS features with {{site.project_title}}
- How {{site.project_title}}'s polyfills shim certain behaviors
Many of the topics outlined in this article are closely related to how CSS and Shadow DOM interact with each other. If you want the dirty details on styling Shadow DOM, see my article on HTML5Rocks.com, Shadow DOM 201 - CSS and Styling.
Most elements in HTML have default styling applied by the browser. For example,
<head> and <title> are display: none, <div> is display: block,
<body> has margin: 8px, and list items are list-style-type: disc.
As with any HTML element, users of your Custom Element can define styles on it:
<style>
x-foo {
display: block;
}
x-foo:hover {
opacity: 0;
}
</style>
<x-foo></x-foo>
However, it's common for a Custom Element to define its own look.
Elements you create will likely need some sort of styling. :host and :host(<selector>) allows you to target and style a custom element internally from within its definition:
<polymer-element name="x-foo" noscript>
<template>
<style>
:host {
/* Note: by default elements are always display:inline. */
display: block;
}
</style>
</template>
</polymer-element>
:host refers to the custom element itself and has the lowest specificity. This allows
users to override your styling from the outside.
The more complex form of :host is :host(<selector>). It allows you to write a
rule that targets the host if it matches <selector>: For example:
<x-foo class="different"></x-foo>
matches
:host(.different) {
...
}
An interesting application of :host is for reacting to different user-driven states (:hover, :focus, :active, etc.):
<polymer-element name="x-button" noscript>
<template>
<style>
:host {
opacity: 0.6;
transition: opacity 400ms ease-in-out;
}
:host(:hover) { opacity: 1; }
:host(:active) { ... }
</style>
<button><content></content></button>
</template>
</polymer-element>
<x-button>x-buttonz!</x-button>
When someone mouses over <x-button> they'll get a sexy fade-in!
Demo: x-buttonz!
The :host-context(<selector>) pseudo class matches the host element if it or any of its ancestors matches <selector>.
Example - color the element if an ancestor has the different class:
:host-context(.different) {
color: red;
}
One reason you might find :host-context() useful is for theming. For example, many people do theming by applying a class to <html> or <body>.
<body class="different">
<x-foo></x-foo>
</body>
Example - theming an element by outside classes
<polymer-element name="x-foo" noscript>
<template>
<style>
:host-context(.different) { ... }
</style>
</template>
</polymer-element>
<body class="different">
<x-foo></x-foo>
</body>
You can dynamically change an element's styling by, you guessed it, modifying
its .style property.
From the outside:
var xFoo = document.createElement('x-foo');
xFoo.style.background = 'blue';
From within the element:
{% raw %} <style> :host { display: inline-block; background: red; color: white; } </style>
Demo:
If you're feeling loco, it's possible to modify the rules in an :host
block using CSSOM:
{% raw %} <style> :host { background: red; } </style> <script> Polymer('x-foo', { changeBg: function() { var sheet = this.shadowRoot.querySelector('style').sheet; // Brittle if :host isn't first in <style>. var hostRules = sheet.cssRules[0]; // Append the rule to the end. hostRules.insertRule(':host:hover { color: white; }', hostRules.cssRules.length); } }); </script> {% endraw %}
The only reason to do this would be to programmatically add/remove pseudo-class rules. It's also worth noting that this trick doesn't work under {{site.project_title}}'s Shadow DOM polyfill. See issue #23.
When you declare <x-foo> (or any non-native HTML element), it exists
happily on the page as a regular HTMLElement. Only when the browser registers its definition
does <x-foo> become magical.
Before an element gets registered, the process of upgrading it may take more time than expected. For example, an HTML Import that defines your element might be slow due to poor network conditions.
To combat these types of UX issues and mitigate things like FOUC, you can use the CSS :unresolved pseudo class. It applies to unknown elements right up until the point the lifecycle createdCallback is called.
Support: CSS :unresolved is supported natively in Chrome 29. If you're using
a browser where it is not available natively, use {{site.project_title}}'s FOUC prevention features.
{: .alert .alert-success}
Example: fade in an element when it's registered
<style>
x-foo {
opacity: 1;
transition: opacity 300ms;
}
x-foo:unresolved {
opacity: 0;
}
</style>
Example: using generated content to display a "loading" message
<style>
:unresolved {
display: flex;
justify-content: center;
background: rgba(255,255,255,0.5);
border: 1px dashed #ccc;
border-radius: 5px;
}
:unresolved:after {
padding: 15px;
content: 'loading...';
color: #ccc;
}
</style>
{{site.project_title}} provides the [unresolved] attribute to polyfill the CSS
:unresolved pseudo class. See FOUC prevention. The attribute is automatically removed from elements at polymer-ready time, the
event that signifies all elements have been upgraded.
Example
<style>
x-foo[unresolved] {
/* custom styling */
}
</style>
<x-foo unresolved></x-foo>
To style the internal markup of an element, include a <link> or <style> tag
inside the topmost <template>:
<polymer-element name="x-foo" noscript>
<template>
<style>
p {
padding: 5px;
}
#message {
color: blue;
}
.important {
font-weight: bold;
}
</style>
<div id="message">I'm a status message!</div>
<p>Web components are great</p>
<footer class="important">That is all</footer>
</template>
</polymer-element>
Scoped styling is one of the many features of Shadow DOM. Styles defined inside the shadow tree don't leak out and page styles don't bleed in.
{{site.project_title}} creates Shadow DOM from the topmost <template>
of your <polymer-element> definition, so styles defined internally to your element
are scoped to your element. There's no need to worry about duplicating an id
from the outside or using a styling rule that's too broad.
Note For browsers that don't support Shadow DOM natively, the polyfill attempts to mimic scoped styling as much as possible. See the polyfill details on scoped styling. {: .alert .alert-info }
If you need to style nodes distributed into your element from the user's Light DOM, see styling distributed nodes.
<content> elements allow you to select nodes from the "Light DOM" and render them at predefined locations in your element. The CSS ::content pseudo-element is a way to style nodes that pass through an insertion point.
Full example
<polymer-element name="x-foo" noscript>
<template>
<style>
content[select="p"]::content * { /* anything distributed here */
font-weight: bold;
}
polyfill-next-selector { content: 'p:first-child'; }
::content p:first-child {
color: red;
}
polyfill-next-selector { content: 'footer > p'; }
::content footer > p {
color: green;
}
polyfill-next-selector { content: ':host > p'; }
::content p { /* scope relative selector */
color: blue;
}
</style>
<content select="p"></content>
<content></content>
</template>
</polymer-element>
<!-- Children of x-foo are the Light DOM. -->
<x-foo>
<p>I'm red and bold</p>
<p>I'm blue and bold</p>
<footer>
<p>I'm green</p>
<span>I'm black</span>
</footer>
</x-foo>
Note: For complex styling like distribute nodes, {{site.project_title}} provides the polyfill-*
selectors to polyfill certain Shadow DOM features. See the Styling reference for more information on the directives.
{: .alert .alert-info }
Remember: styles defined in the main document continue to apply to the Light DOM nodes they target, even if those nodes are distributed into Shadow DOM. Going into an insertion point doesn't change what styles are applied. An example helps illustrate this point:
<style>
x-foo > div {
color: green;
}
.red {
color: red;
}
</style>
<polymer-element name="x-foo" noscript>
<template>
<div class="red">Shadow DOM: shouldn't be red (under native Shadow DOM)</div>
<content select="div"></content>
</template>
</polymer-element>
<x-foo>
<div>Light DOM: green</div>
</x-foo>
Demo:
The element's Shadow DOM <div class="red"> does not match the .red class.
The distributed <div>Light DOM: green</div> remains green because
it's logically still in the parent page and therefore matching x-foo > div.
It's simple being rendered elsewhere (over in Shadow DOM land).
The ::shadow pseudo-element and the /deep/ combinator pierce through Shadow DOM's boundaries and allow you to style elements within different shadow trees.
If an element has at least one shadow tree, the ::shadow pseudo-element matches the shadow roots themselves.
For example, say you wanted to style x-foo's internal p element. Writing x-foo::shadow selects x-foo's shadow root. From there, you can write a normal descendant selector to get at the p:
<style>
x-foo::shadow p {
color: red;
}
/* Equivalent to previous rule (in this case). */
x-foo::shadow > p {
color: red;
}
</style>
<polymer-element name="x-foo" noscript>
<template>
<p>I am red!</p>
<content></content>
</template>
</polymer-element>
<x-foo>
<p>I am not red (under native shadow dom).</p>
</x-foo>
Demo:
<style shim-shadowdom> x-foo-shadow::shadow p { color: red; } </style>I am not red (under native shadow dom).
In this example, <p>I am not red (under native shadow dom)</p> remains unstyled because the x-foo::shadow p { ... } rule only targets the <p> internal to x-foo (e.g. in its Shadow DOM). Under the polyfill, it is styled red. This is because {{site.project_title}} replaces the ::shadow, rewriting the rule to be x-foo p.
A more full fledged example is styling a tabs component, say <x-tabs>. It has <x-panel> children in its Shadow DOM, each of which has an h2 heading. To style those headings from the main page, one could use the ::shadow pseudo-element like so:
{%raw%} <style> x-tabs::shadow x-panel::shadow h2 { ... } </style>
<polymer-element name="x-tabs" noscript>
<template>
<x-panel heading="Title">
<p>Lorem Ipsum</p>
</x-panel>
...
</template>
</polymer-element>
<polymer-element name="x-panel" attributes="heading" noscript>
<template>
<h2>{{heading}}</h2>
<content>No content provided.</content>
</template>
</polymer-element>
<x-tabs></x-tabs>
{%endraw%}
The /deep/ combinator is similar to ::shadow, but more powerful. It completely ignores all shadow boundaries and crosses into any number of shadow trees.
Example style all h2 elements that are descendants of an <x-tabs>, anywhere in a shadow tree:
x-tabs /deep/ h2 {
...
}
Example style all elements with the class .library-theme, anywhere in a shadow tree:
body /deep/ .library-theme {
...
}
There are a bunch of new concepts when it comes to styling Custom Elements. What makes things particularly interesting (and at the same time, wonky) is this Shadow DOM guy. In web development, we're conditioned to think, "Yay! Globals everywhere!" That goes for DOM, CSS, and JS. Not so with Custom Elements. It's a brave new world, but a powerful one of encapsulation, puppies, and fluffy bunnies. Drink it in.
{% include disqus.html %}