Skip to content

[css-scoping] Breaking name encapsulation #10808

@tabatkins

Description

@tabatkins

The Scoping spec defines "tree-scoped names" and "tree-scoped references" to explain the encapsulation effects of Shadow DOM on CSS constructs. These concepts have been reused across many specs now, and overall seem to do what we want - shadows can safely define and reference names without having to worry about the outer page accidentally defining or referencing a conflicting name.

However, authors regularly ask for ways to defeat this encapsulation, and reference things across shadows. Generally this is because they're using shadow dom just as an organization tool, not an encapsulation boundary - it's the easiest way to use custom elements. It's possible that the correct answer to these requests is that we need a way to opt a shadow tree out of being encapsulated, so it acts like it's part of the outer page as much as possible (no more event censoring, etc either).

However, in the absence of that, it might make sense to have a CSS mechanism for this opt-out, to give authors a way to (a) write shadow CSS that can refer to names defined in the outer tree, or in other shadow trees entirely, and (b) write shadow CSS that defines names that the outer page, or other shadow trees, can reference.

As a conversation starter, I suggest a new global(<ident>, <ident>?) function, allowed anywhere that takes idents for tree-scoped names or tree-scoped references. If an ident foo would define a tree-scoped name, global(foo) defines it "globally" instead, without a tree scope. Similarly, if an ident foo would be a tree-scoped reference, then global(foo) is a "global" reference, matching names without a tree scope.

The second argument is a scope name; the name/ref is still global, but must match the scope name as well as the ident to be considered matching. Omitting the ident indicates the "default" global scope, and still only matches with other names/refs in the "default" global scope.

(Another way of looking at this is that global() just changes the way we construct a tree-scoped name/ref. Normally it's a (name, scope) pair with the scope being an automatically-filled-in tree reference; if you use global(), we instead set the scope to the given ident. Matching rules still work otherwise the same, requiring both names and scopes to match. )

For example, by default, using anchor-name: --foo; on an element in a shadow DOM, and then position-anchor: --foo; in a light-dom positioned element, the two elements won't see each other. Each is tree-scoped, and the anchor-matching rules require the name and the reference to have the same tree scope. But if you used anchor-name: global(--foo); and position-anchor: global(--foo);, they'd find each other, because they have matching tree scopes (the default global scope).

Coordinating components can still avoid accidentally clashing with globals from other sources by using the scope-name argument with a reasonably unique name, if they want, and pages can achieve hostile interoperability with those components by using the same scope name if they want.

(Note: The first draft of the tree-scoped idea, back in #1995, proposed something similar, but not as good. We ended up concluding that we didn't need the complexity of letting references switch into "global" mode at the time, but I think continuing author requests shows that was a wrong decision in the end.)

(Also, if we did this, we should probably define <tree-scoped-ref> and <tree-scoped-name> productions, and do an audit to use those everywhere we're currently just using idents and declaring the tree-scoped-ness in prose.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Status

    Thurs afternoon

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions