html icon indicating copy to clipboard operation
html copied to clipboard

Exploration of exposing stylesheets across scopes

Open KurtCattiSchmidt opened this issue 6 months ago • 3 comments

What is the issue with the HTML Standard?

During the discussion of https://github.com/whatwg/html/issues/11019 at Thursday's sync, it was suggested that we discuss some potential proposals for global ID references and how they might work with this feature. Here are a few starting points. This is intended to be an open discussion, so please feel free to add more ideas!

1. Referencetarget inspired solution on <template>

This proposal would allow for

<template exportids="foo"><!-- Exports 'foo' to the light DOM -->
  <template exportids="foo, bar">
    <style id="foo">
     ...
    </style>
  </template>
  <link rel="stylesheet" href="#bar"> <!-- 'bar' is in scope due to `exportids` -->
</template>
<link rel="stylesheet" href="#foo"><!-- 'foo' is in the global scope, reference matched -->
<link rel="stylesheet" href="#bar"><!-- 'bar' is not in scope, reference not matched -->

This works nicely for style sharing, however it doesn't address the streaming SSR scenario where ID's may need to be exported from a deeply nested shadow root. This is because the parent <template> has already been emitted and may not have specified exportid's. This could be addressed with wildcard support on exportid's:

<template exportids="global-*"><!-- Exports any id's beginning with 'global-' to the light DOM -->
  <template exportids="global-*"><!-- Exports any id's beginning with 'global-' to the parent shadow root -->
    <style id="global-foo"><!-- Streaming SSR exports a global id -->
    ...
    </style>
  </template>
  <link rel="stylesheet" href="#global-foo"> <!-- 'global-foo' is in scope due to wildcard `exportids` -->
</template>
<link rel="stylesheet" href="#global-foo"> <!-- 'global-foo' is in scope due to wildcard `exportids` -->

This is my preferred solution because it's not limited to style sharing. I would expect that the exported ID's from shadow roots would work everywhere ID's can be referenced in HTML, including in-page anchor references, <link rel="blocking">, and even SVG xlink:href references.

2. XID (https://github.com/WICG/webcomponents/issues/939#issue-971914425)

This proposal would be a GUID (or MD5 hash) of the style contents. I assume that some sort of prefix before the XID will be required where it's referenced, so as not to collide with existing valid identifiers.

<template shadowroot="open">
  <style type="adopted-css" xid="x61h8cys"> <!-- xid identifiers are global -->
   ...
  </style>
</template>

<template shadowroot="open" adopted-styles="">
  <link rel="stylesheet" href="xid:x61h8cys"> <!-- references global xid -->
</template>

Since this proposal expects a hash, there is a question as to what happens if the referenced object changes (in this case, the shared styles being modified). This could be addressed by either requiring that the styles referenced are static (which differs the conclusion we came to on Thursday about how style sharing should work), or perhaps the hash could dynamically update all existing references if the target is updated.

3. Global prefix on idrefs

The idref definition could be updated to support some sort of special global prefix:

<template>
  <template>
    <style id="foo">
    ...
    </style>
  </template>
  <link rel="stylesheet" href="global:#foo">
</template>

The specific global prefix is open to suggestions. A double-octothorpe (##) could be an intuitive alternative. The definition for identifiers allows for any ASCII character, so any option is a potential compatibility-breaking change: https://dom.spec.whatwg.org/#concept-id

This would work nicely with the style sharing scenario, but I have some concerns from an implementation standpoint. This has additional costs of either 1) doing a traversal of all shadow roots to query the element ID or 2) Always storing elements-by-id twice, once in their own tree scope and again globally, even if global references aren't used anywhere.

KurtCattiSchmidt avatar Jun 09 '25 23:06 KurtCattiSchmidt

Thanks Kurt!

I want to point out what I think is a critical detail: we already have cross-scope style sharing in the form of adoptedStyleSheets. What we're looking for is a way to serialize and deserialize adoptedStyleSheets to and from HTML without JavaScript.

justinfagnani avatar Jun 11 '25 20:06 justinfagnani

To me, a key question to answer is how would any algorithm -- 1, 2, 3 or probably any other -- handle duplicate identifiers.

It is hand-wavy to say that duplicate identifiers are just not allowed so it does not matter, because users can still and will still do it. There is no way to stop this. In case of a duplicate identifier, some resolution (or complete failure) must be specified.

For example: what colors "win" below (modify for preferred syntax)? And why?

<style id="shared_styles">
  p { color: red; }
</style>
<my-container>
  <template shadowrootmode="open">
    <style id="shared_styles">
      p { color: blue; }
    </style>
    <my-element>
      <template shadowrootmode="open">
        <style id="shared_styles">
          p { color: green; }
        </style>
        <link rel="stylesheet" href="#shared_styles" >
        <p>what color am I/</p>
      </template>
    </my-element>
    <link rel="stylesheet" href="#shared_styles" >
    <p>what color am I/</p>
  </template>
</my-container>
<link rel="stylesheet" href="#shared_styles" >
<p>what color am I/</p>

One obvious way among others is that first in document order (expanded to include shadow trees) wins. This is the way in essence that HTML and CSS handle duplicate IDs now (within the current document scope and minus shadow trees of course).

Also, I note that some ideas are not polyfillable and only support declarative shadow DOM.

robglidden avatar Jun 12 '25 01:06 robglidden

I would say "red red red" is the best answer above, i.e. first <style id> in tree order expanded to include shadow trees.

Advantages:

  • fully supports streaming SSR (HTTP Transfer-Encoding: chunked)
  • does not require a UUID or hash (both problematic for several reasons under Web Platform Principles) or a new identifier attribute
  • logically extends the long standing "first element, in tree order, within this’s descendants" resolution algorithm
  • no backwards compatibility issue, since local references to style elements are now undefined, and this is a different algorithm and element for a different purpose than exisitng id-attribute-using algorithms
  • extensible to other element types, like SVG <use href>
  • works with "3. Global prefix on idrefs" if is desired to have an opt-in-to-global for either <style>s and/or <link>s in shadow trees (I personally prefer "##" as a global prefix on <link>)
  • could also be used to provide a DOM API method to actually globally find the id'ed <style> element (see getElementByGlobalId() below)
  • could provide a CSS supports capability if concurrently implemented for SVG <use href>: CSS.supports("clip-path: ##") could return true instead of false as it does now.
  1. Global prefix on idrefs ... This would work nicely with the style sharing scenario, but I have some concerns from an implementation standpoint. This has additional costs of either 1) doing a traversal of all shadow roots to query the element ID or 2) Always storing elements-by-id twice, once in their own tree scope and again globally, even if global references aren't used anywhere.

Would recursive traversal into shadow trees really be so additionally costly? Even already and necessarily for regular old light DOM, "[i]n tree order is preorder, depth-first traversal of a tree", so any map optimization still needs to be in tree order.

This polyfill implements a short recursion-wrapped DOM TreeWalker. It extends the algorithm for getElementById(elementId):

"The getElementById(elementId) method steps are to return the first element, in tree order, within this’s descendants, whose ID is elementId; otherwise, if there is no such element, null."

to getElementById(elementId, global = false, elementType = null).

robglidden avatar Jun 18 '25 23:06 robglidden

I want to point out what I think is a critical detail: we already have cross-scope style sharing in the form of adoptedStyleSheets. What we're looking for is a way to serialize and deserialize adoptedStyleSheets to and from HTML without JavaScript.

Adopted or linked, wouldn't first <style id> in tree order expanded to include shadow trees would work equally?

Also a link supports more CSS cascade use cases (adoptedStyleSheets come after).

And link is polyfillable, whereas an adopting attribute on a declarative shadow DOM template is not.

robglidden avatar Jun 20 '25 00:06 robglidden

I think the solution to serializing/deserializing adopted stylesheets should be additional options in the getHTML / setHTML area rather than a new HTML concept like a global ID.

There could be two main ways to marshal adopted stylesheets:

  1. Flatten them into @sheet blocks in a proper style element (or to individual style elements), and reference them as @import blocks in the appropriate shadow roots. This changes the semantics of adoptedStylesheets but is fully representable as a string.
  2. Save them into an array of adoptable stylesheets during getHTML, with some temporary references from the shadow roots, and resolve those references in setHTML back as adoptedStylesheets. This is lossless in terms of keeping the adopted stylesheets semantics, with the caveat that the intermediate representation is more involved than a string.

Both of these options keep the problem encapsulated in the realm of serialization/deserialization and doesn't leak this complexity to other parts of the web platform.

In either case, I don't think this should block #11019. If anything, having a semantic to address inline styles can help with option 1 I put here.

noamr avatar Jul 01 '25 08:07 noamr

Another option that I find perhaps compelling is to have a global ID, but have it be there only while parsing and serializing, kind of like the shadowRootMode attribute. It disappears from the DOM after parsing, and is only used to match adoptedStylesheet instances that repeat themselves in the same parser session.

Something like this:

  • when serializing the DOM using getHTML, every stylesheet gets a comment at its start with this parser-session-specific unique ID. Other serializers are free to do the same.
  • A <template shadowrootmode> can also have an shadowRootAdoptedStylesheets attribute that is similarly a parser-only direcive.
  • That new attribute is a space-separated list of IDs pointing to the IDs from the stylesheet comments.

noamr avatar Jul 14 '25 12:07 noamr

  • That new attribute is a space-separated list of IDs pointing to the IDs from the stylesheet comments.

Maybe I don't understand the idea of comment-based global <style> IDs correctly, but it seems to me that even if a global ID is in a comment above (or in?) the <style> element, the referencing algorithm would still need to resolve duplicate IDs:

<!-- global-id: shared_styles -->
<style>
  p {
    color: blue;
  }
</style>
<!-- global-id: shared_styles -->
<style>
  p {
    color: red;
  }
</style>
<my-element>
  <template shadowrootmode="open" shadowRootAdoptedStylesheets="shared_styles">
    <p>what color am I/</p>
  </template>
</my-element>

I think this means that placing a global ID in a comment wouldn't really add anything over just using the <style id> attribute, which would need to be referenced by the same purpose-specific deduping algorithm anyway.

Please correct my misunderstanding.

robglidden avatar Jul 15 '25 23:07 robglidden

@robglidden the way we would avoid duplicate IDs in our SSR system is to either keep a cache of IDs user for each CSSStyleSheet object, or use a content digest as the ID.

justinfagnani avatar Jul 16 '25 00:07 justinfagnani

@justingnani: server-generated unique IDs, machine-generated or otherwise, would of course still work fine as they do now.

But that in itself does not create a need to require only machine-generated IDs over the basic Web Platform Design Principle to Design textual formats for humans when potentially-duplicate human-readable and human-generated formats still exist and need to be referenced.

robglidden avatar Jul 16 '25 01:07 robglidden

  • That new attribute is a space-separated list of IDs pointing to the IDs from the stylesheet comments.

Maybe I don't understand the idea of comment-based global <style> IDs correctly, but it seems to me that even if a global ID is in a comment above (or in?) the <style> element, the referencing algorithm would still need to resolve duplicate IDs:

I think this means that placing a global ID in a comment wouldn't really add anything over just using the `

It wouldn't because unlike <style id>, those IDs would be readable only when deserializing. However, thinking about this further I think this approach would be cleaner and aligned with the "detached" nature of adopted stylesheets.

noamr avatar Jul 16 '25 08:07 noamr