[css-scoping] Handling global name-defining constructs in shadow trees · Issue #1995 · w3c/csswg-drafts (original) (raw)
Since the introduction of Shadow DOM, we've been struggling with what to do with name-defining things, like @font-face, which define global names for other things to reference.
- Can @font-face be used in shadow trees?
- Does it still define a global name? What's our plan to deal with collisions between non-communicating components?
- Does it define a scoped name? What's our plan to deal with collisions between an outer and inner @font-face name, when the font-family property inherits across a boundary?
Right now the answer is a collective shrug. I think I have an answer, however:
- Every name-defining thing (such as
@font-face
) is valid inside of shadow trees, and is scoped to the TreeScope that its defining stylesheet is in. Nested shadows can use name-defining things defined in higher trees (see below), but can't directly refer to them. - Every reference to a defined name (such as a
font-family
font name) is implicitly a tuple of (name, defining scope), where the defining scope is the TreeScope the reference's stylesheet is in. (In other words, it's always a reference to the local thing defining the name, not something further up the scope tree.) - Applying a style from a stylesheet in one TreeScope to an element in a different TreeScope (such as via
::part()
) thus resolves the name against the stylesheet's TreeScope, even if the element's TreeScope has that name redefined to something else. (So an outer page setting an::part(foo) { animation: foo 1s; }
uses the@keyframes foo {...}
from the outer page, not anything from inside the shadow tree that that "foo" part is in.) - The value/scope tuple is inherited normally thru TreeScopes, meaning that a particular reference refers to the same name-defining construct no matter how deeply it gets inherited.
- This does not affect how the value serializes - the reference continues to serialize as just a keyword, with no tree scope mentioned. So setting a property on an element to its own computed value is not always a no-op when shadow DOM is involved; the new value will be referring to the element's tree scope, which may not have that name defined or have it defined to something else.
- This does affect how the value reifies in TypedOM - keywords will gain a nullable
.scope
attribute or something, which points to the tree scope the keyword is being resolved against.
This has some implications. Since the defining scope is implicitly captured by a reference, it doesn't change as the value inherits. Thus, in this situation:
ONE <::shadow>
TWO
THREE
FOUR </> </> </>
- ONE is rendered in the outer "foo" font (standard behavior)
- TWO is rendered in the outer page's "foo" font (via inheritance, since the outer scope was captured at definition time)
- THREE is rendered in the shadow's "foo" font (specified via a style in the shadow tree, thus implicitly capturing the shadow as its scope)
- FOUR is rendered in the outer page's "foo" font (specified via a style in the outer page, thus implicitly capturing the outer page as its scope)
Scripting is a slightly thornier problem here. When setting styles, we can use the rules I've already laid out - you're always setting the style in some stylesheet (perhaps the implicit one attached to an element and accessed via el.style
), so there's a consistent notion of an associated TreeScope. (This may not always be obvious, but it's clear - a script that pokes around inside of the shadows of its components and sets styles needs to be aware of what scope the stylesheet is in and what scope the name-defining thing it's trying to reference is in.)
(Edited to take into account the compromise to drop the scoped() syntax and only allow implicit references via the string-based API.)