Cross-scope styling

Background

Shadow DOM (and its approximation via Shady DOM) bring much needed benefits of
scoping and style encapsulation to web development, making it safer and easier
to reason about the effects of CSS on parts of your application. Styles do not
leak into the local DOM from above, and styles do not leak from one local DOM
into the local DOM of other elements inside.

This is great for protecting scopes from unwanted style leakage. But what
about when you intentionally want to customize the style of a custom element's
local DOM, as the user of an element? This often comes up under the umbrella of
"theming". For example a "custom-checkbox" element that may internally use a
.checked class can protect itself from being affected by CSS from other
components that may also happen to use a .checked class. However, as the user
of the checkbox you may wish to intentionally change the color of the check to
match your product's branding, for example. The same "protection" that Shadow
DOM provides at the same time introduces a practical barrier to "theming" use
cases.

Deprecated shadow DOM selectors. One solution the Shadow DOM spec authors
provided to address the theming problem was the /deep/ combinator and ::shadow
pseudo-element, which allowed writing rules that pierce through the Shadow DOM
encapsulation boundary. However, these proved problematic and have been deprecated.

Rather than exposing the details of an element's internal implementation for
theming, instead an element author defines one or more custom CSS
properties as part of the element's API.

These custom properties can be defined similarly to other standard CSS properties
and will inherit from the point of definition down the composed DOM tree,
similar to the effect of color and font-family.

In the simple example below, the author of my-toolbar identified the need for
users of the toolbar to be able to change the color of the toolbar title. The
author exposed a custom property called --my-toolbar-title-color which is
assigned to the color property of the selector for the title element. Users
of the toolbar may define this variable in a CSS rule anywhere up the tree, and
the value of the property will inherit down to the toolbar where it is used if
defined, similar to other standard inheriting CSS properties.

<dom-moduleid="my-element"><template><style>/* Make all toolbar titles in this host green by default */:host{
--my-toolbar-title-color: green;
}/* Make only toolbars with the .warning class red */.warning{
--my-toolbar-title-color: red;
}</style><my-toolbartitle="This one is green."></my-toolbar><my-toolbartitle="This one is green too."></my-toolbar><my-toolbarclass="warning"title="This one is red."></my-toolbar></template><script>
Polymer({ is: 'my-element'});
</script></dom-module>

The --my-toolbar-title-color property only affects the color of the title
element encapsulated in my-toolbar's internal implementation. In the
future the my-toolbar author can rename the title class or
restructure the internal details of my-toolbar without changing the custom
property exposed to users.

You can also include a default value in the var() function, to use in case the user
doesn't set the custom property:

color: var(--my-toolbar-title-color, blue);

Thus, custom CSS properties introduce a powerful way for element authors to
expose a theming API to their users in a way that naturally fits right alongside
normal CSS styling. It is already on a standards track (currently in the candidate recommendation (CR) stage of the process) with support in Firefox, Safari, and Chrome 49.

Custom CSS mixins

It may be tedious (or impossible) for an element author to predict every
CSS property that may be important for theming, let alone expose every
property individually.

The custom properties shim includes an extension that enables an element
author to define a set of CSS properties as a single custom property and
then allow all properties in the set to be applied to a specific CSS rule
in an element's local DOM. The extension enables this with a mixin capability
that is analogous to var, but which allows an entire set of properties
to be mixed in. This extension adheres to the
CSS @apply rule
proposal.

Use @apply to apply a mixin:

@apply --mixin-name;

Defining a mixin is just like defining a custom property, but the
value is an object that defines one or more rules:

Older @apply syntax. The @apply syntax was originally implemented in Polymer using
parenthesis: @apply(--mixin-name). Polymer 1.6.0 and later accepts @apply
without parenthesis, matching the proposal. You can continue using the older syntax in Polymer 1.x,
but starting in Polymer 2.0, only the newer syntax (without parenthesis) is accepted.

Custom property API for Polymer elements

Polymer's custom property shim evaluates and applies custom property values once
at element creation time. In order to have an element (and its subtree) re-
evaluate custom property values due to dynamic changes such as application of
CSS classes, etc., call the updateStyles
method on the element. To update all elements on the page, you can also call
Polymer.updateStyles.

You can directly modify a Polymer element's custom property by setting
key-value pairs in customStyle
on the element (analogous to setting style) and then calling updateStyles.
Or you can pass a dictionary of property names and values as an argument to
updateStyles.

Custom properties shim limitations

Cross-platform support for custom properties is provided in Polymer by a
JavaScript library that approximates the capabilities of the CSS Variables
specification for the specific use case of theming custom elements, while
also extending it to add the capability to mixin property sets to rules as
described above. For performance reasons, Polymer does
not attempt to replicate all aspects of native custom properties.
The shim trades off aspects of the full dynamism possible in CSS in the
interests of practicality and performance.

Below are current limitations of the shim. Improvements to performance and
dynamism will continue to be explored.

Dynamism limitations

Only property definitions which match the element at creation time are applied.
Any dynamic changes that update property values are not applied automatically. You
can force styles to be re-evaluated by calling the
updateStyles method on a
Polymer element, or Polymer.updateStyles to update all element
styles.

Inheritance limitations

Unlike normal CSS inheritance which flows from parent to child, custom
properties in Polymer's shim can only change when inherited by a custom element
from rules that set properties in scope(s) above it, or in a :host rule for
that scope. Within a given element's local DOM scope, a custom property can
only have a single value. Calculating property changes within a scope would be
prohibitively expensive for the shim and is not required to achieve cross-scope
styling for custom elements, which is the primary goal of the shim.

<dom-moduleid="my-element"><template><style>:host{
--custom-color: red;
}.container{
/* Setting the custom property here will not change *//* the value of the property for other elements in *//* this scope. */--custom-color: blue;
}.child{
/* This will be always be red. */color:var(--custom-color);
}</style><divclass="container"><divclass="child">I will be red</div></div></template><script>
Polymer({ is: 'my-element'});
</script></dom-module>

All features of custom-style are available when defining styles as part of
Polymer elements (for example, in <style> elements within a custom element's
<dom-module>). The exception is the :root selector, which is only useful at
the document level. The custom-style extension should only be used for
defining document styles, outside of a custom element's local DOM.

Shared styles and external stylesheets

To share style declarations between elements, you can package a set
of style declarations inside a <dom-module> element. In this section,
a <dom-module> holding styles is called a style module for convenience.

A style module declares a named set of style rules that can be imported into
an element definition, or into a custom-style element.

Note: Style modules were introduced in Polymer 1.1;
they replace the experimental support for external stylesheets.

Define a style module inside an HTML import using the <dom-module>
element.

A single style tag can both include shared styles
and define local rules:

<styleinclude="shared-styles">:host{ display: block; }</style>

(This works for both custom-style elements and <style> tags inside
custom elements.) The shared styles are applied before the styles defined
inside the body of the <style> tag, so the shared styles can be overridden
by the styles defined in the body.

External stylesheets (deprecated)

Deprecated feature. This experimental feature is now deprecated in favor of
style modules. It is still supported, but support will
be removed in the future.

Polymer includes an experimental feature to support loading external stylesheets
that will be applied to the local DOM of an element. This is typically
convenient for developers who like to separate styles, share common styles
between elements, or use style pre-processing tools. The syntax is slightly
different from how stylesheets are typically loaded, as the feature leverages
HTML Imports (or the HTML Imports polyfill, where appropriate) to load the
stylesheet text such that it may be properly shimmed and/or injected as an
inline style.

To include a remote stylesheet that applies to your Polymer element's local DOM,
place a special HTML import <link> tag with type="css" in your <dom- module> that refers to the external stylesheet to load.

Third-party libraries that modify local DOM

If you are using a third-party library that adds local DOM nodes to your
Polymer element, you may notice that styles on the element do not update
properly.

The correct way to add DOM nodes to a Polymer element's local DOM is via
the Polymer DOM API. This API lets you manipulate
nodes in a way that respects the local DOM and ensures that styles are
updated properly.

When using third-party libraries that do not use the Polymer DOM
API, use the scopeSubtree
method to apply proper CSS scoping to a node and all of its descendants.

Create a container node inside your element's local DOM, and have your
third-party library create DOM under that container node.

containerNode is the root node of the tree you wish to scope. Setting
the second argument to false scopes the specified node and descendants
once. Setting it to true enables a mutation observer that applies CSS
scoping whenever containerNode or any of its descendants are modified.

Not for use on Polymer elements. If the subtree that you scope
contains any Polymer elements with local DOM, scopeSubtree will
cause the descendants' local DOM to be styled incorrectly.