- From: Tab Atkins Jr. <jackalmage@gmail.com>
- Date: Sun, 17 Nov 2013 10:16:58 -0800
- To: www-style list <www-style@w3.org>
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