[shadow-dom] Update on Shadow DOM's interaction with CSS

I just realized I haven't sent an update about shadow DOM in quite a
while.  Let's fix that!

After extensive discussion, rejiggering, implementing, testing, and
repeats of this cycle multiple times, we've finally settled on a
syntax and ability set that seems to work right, without confusion,
and neither adding too many or too little restrictions for real-world
content.  This has been stable for months after lots of banging and
testing, so we're pretty confident about it now.

This email is long, so here's a quick summary:

1. The "hat" combinator ^ pierces one shadow boundary, and then
selects all descendants inside the shadow.
2. The "cat" combinator ^^ pierces arbitrary shadow boundaries, and is
a descendant combinator at all levels.
3. Inside a shadow root, the host element is part of the tree, but
defined to not match *any* selector except :host.  (Including :not(*),
if we can swing it.)
4. The :host(compound-selector) functional pseudo-class matches the
host element iff it or an ancestor (all the way up to the document
root) matches the selector.
5. The ::content pseudo-element switches into the distributed nodes
for a given <content> element.
6. The :top pseudo-class matches elements without element parents.
(This is a generalization of the :root concept, because CSSWG doesn't
want to generalize :root itself.)

As a reminder, shadow DOMs are, by default, sealed against outside
CSS.  They exist in a different context that outer-document selectors
can't match into.  (You can relax this, but it's not the default.)

There are some cases where you need to have styles poke into a shadow
DOM, though.  Some of these cases can be solved by Variables, as they
inherit into a shadow DOM by default.  Just define a couple of theming
variables that your component uses, and authors can set those on the
component root.

That's not enough, though.  Sometimes you want to allow arbitrary
styling of stuff inside a shadow DOM, but just want it to be
intentional, rather than accidentally leaking in.  Previously, we
tried to do this with the ::part pseudo-element and manually declaring
that certain elements in your shadow were surfaced as ::part, but this
proved too limiting.  Some use-cases demand arbitrary styling of
everything, which is inconvenient.  When using Shadow DOM for real
applications, it also ended up being necessary to reach down through
more than one shadow, as you sometimes refactor things into components
for organization reasons rather than to enforce any strict separation.

The solution we came up with several months ago and which has proved
stable are two new combinators, affectionally named "hat" and "cat",
and a few related features.

## Hat And Cat Combinators ##

The "hat" or "shadow" combinator is spelled ^ and pierces one shadow
boundary.  "foo ^ bar" matches if and only if foo is an element
hosting a shadow root, and bar is inside foo's shadow tree.  Note that
it matches all elements inside the shadow tree by default, like a
descendant combinator; "foo ^ *" matches *every* element in the shadow
tree.  See :host, below, for how to select more precisely.  This is
the standard way to style the contents of a Web Component from the
outside.

The "cat" or "general shadow" combinator is spelled ^^ and pierces an
arbitrary number of shadow boundaries.  It also acts like a descendant
combinator, but from the start: in "foo ^^ bar" foo doesn't have to be
a component - it could have a descendant that's a component and holds
bar.  In other words, it's a standard descendant combinator, with the
definition of "child" expanded to be "child, or contents of a shadow
tree".  This is a powerful, dangerous combinator, but it's proven
necessary for lots of common use-cases.  Without it, the Polymer folk
just hacked around the lack in silly ways.  It's most commonly used in
the root document to select all components of a certain type,
regardless of where they are in the tree, and style them in a certain
way, like ":root ^^ foo-button ^ bar { ... }".  (We tried to do
something more controlled, where components surfaced their inner
components with a manual switch, but it ended up being horrible and
hard to use.)

Both of these act similarly to the scoping cascading rules - styles
from an outer tree win over styles from an inner tree.  This is the
opposite of how scoped styles work (which has inner scopes beating
outer scopes), but the same when you instead consider it along a
defaults->customization axis rather than an outer->inner axis.  Outer
trees only poke into inner trees when they're trying to override
defaults.

## Selecting the Host Element ##

Next, we had the problem of what to do with the host element.  You
want the component to be able to style its host element, but the host
isn't actually inside the shadow tree.  The host element is also
"leaky" - you don't have control over its markup, so it might
accidentally have the same class that you're using inside the
component for something else, or even the same ID (IDs must be unique
per-tree, but the host is in a different tree...).

The solution we eventually came up with is that the host element does
show up in the shadow tree for selecting purposes, but as a completely
anonymous node.  There is no way to select it with any selector except
the :host pseudoclass.  If it's acceptable, we'd like to avoid it
being matched by :not(), too, but that's not a strict requirement.
(For predictability reasons - if you switch from, say, "div, p,
details {...} input {...}" to ":not(input) {...} input {...}" or
something, we don't want the host element to suddenly get matched.)

The :host pseudo-class matches the host element inside a shadow tree,
and is the only way to do so.  It serves no other purpose.  Note that
you can select down into the shadow tree from the host, like ":host >
p" to find the top-level p elements.  (You can also use the :top
pseudo-class for this.)

We also consistently found a need to be able to pass theming
information *into* a component without doing the theming yourself. In
other words, you need to be able to style your component differently
based on some outside context.  The solution we came up with is the
:host() functional pseudo-class.

:host(<compound-selector>) matches the host element if it, or one of
its ancestors (hopping up shadow roots all the way to the document
root), matches the compound selector.  This way, you can do something
like placing class="light-theme" on some element in your document, and
any components that know about that class can switch their styling
with ":host(.light-theme) p { background: white; }" etc.  :host() (no
argument) is identical to :host.  Note that the host element is not
anonymous while being matched against this selector.

There are some use-cases that want to detect classes/attrs/tagname/etc
on the host element itself, not one of its ancestors.  This can be
done by combining the two forms, in a slightly awkward but workable
fashion:  while ":host(.foo)" matches if the host element or one of
its ancestors has a "foo" class, ":host(.foo:host)" matches only if
the host element itself has a "foo" class.

## Selecting Distributed Elements ##

This is an older feature that's been around for a while, but under a
different name.  When you distribute light-dom elements into a shadow
DOM with the <content> element, those elements still aren't actually
part of the shadow tree, and for good reason.  But you do sometimes
want to style them, so you need a way to select them.  Previously, we
had a ::distributed pseudo-element hanging off of <content> elements,
but that wasn't a great name (Daniel complained about it in a thread).
 We've renamed it to ::content.

## Selecting "Top-level" Elements ##

We've ended up exposing two different trees (shadow tree, and
distributed tree inside <content>) whose root is not an element.  This
makes it difficult to select "top-level" elements, or do any other
selection that wants to be rooted from the top.

To fix this, we propose a :top pseudo-class that matches elements
without element parents.  This matches the "top-level" elements
distributed by <content> (in that context, their parent is the
distribution root, or whatever it's called), and the top-level
elements inside a shadow tree.

(Shadow-tree top-level elements do technically have a non-element
shadow root parent, but note that :host also acts like it's a root for
all of them.  We feel this is okay.)

~TJ

Received on Sunday, 17 November 2013 18:17:45 UTC