Skip to content

[css-scoping] Handling global name-defining constructs in shadow trees #1995

Closed
@tabatkins

Description

@tabatkins

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:

  1. 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.
  2. 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.)
  3. 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.)
  4. 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.
  5. 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.
  6. 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:

<style>
@font-face { font-family: foo; ... }
body { font-family: foo; }
x-component::part(my-p) { font-family: foo; }
</style>
<body>
 <p>ONE
 <my-component>
  <::shadow>
    <style>
    @font-face { font-family: foo; ... }
    p.foo { font-family: foo; }
    </style>
    <p>TWO
    <p class=foo>THREE
    <p part=my-p>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.)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions