Open justinfagnani opened 3 years ago
@justinfagnani is your xid
conceptually a DOMTokenList or something similar -- that is, can you import 2? This feels related to at least some of the discussions in #909 - this "idea" at least would at least probably provide a way to achieve use cases for things like those which aren't even 'secret' as much as they are 'shorthand for well-known constructs' like an element that allows markdown, asciimath, or LaTeX and it similar to the thing I created and have used for my own takes on this..
Yes, shadow roots need to be able to reference multiple style sheets.
I don't personally see this as that much related to #909 ("open-styleable" shadow roots). Rather than allowing anything from the outside to reach into a shadow root, this is just attempting to replicate JS references to these objects in HTML. it shouldn't be loosening encapsulation at all.
Another option instead of - or maybe in addition to - cross-scope refs is to allow inlining of all module types, keying them by URL that adds them to the module cache as if they had been imported. This would allow a page to render with the necessary CSS modules, attach them to the correct scopes, and later to load JS modules that import them without double-loading them.
<script>
tags with type
values that match import assertion types would support a new attribute that gives the specifier/URL. Something like:
<script type="css" specifier="/foo.css">
:host {
color: red
}
</script>
A later import: import styles from '/foo.css' assert {type: 'css'}
would load the CSS module script defined by the tag.
Full example:
<my-element>
<template shadowroot="open" css-modules="/foo.css /bar.css">
<script type="css" specifier="/foo.css">
:host {
color: red
}
</script>
<!-- ... -->
</template>
</my-element>
<my-element>
<template shadowroot="open" css-modules="/foo.css /bar.css">
<!-- ... -->
</template>
</my-element>
This would have to be made coherent with import maps, but it would allow a page to be rendered with correct scoped styles w/o FOUC and w/o duplication of CSS text.
Being <template shadowroot="open">
creates a shadow root that ID resolution from above does not have access to, should the "expected" version of:
<my-element>
<template shadowroot="open" adopted-styles="x61h8cys">
<style type="adopted-css" xid="x61h8cys">
:host {
color: red
}
</style>
<!-- ... -->
</template>
</my-element>
Actually be:
<style type="adopted-css" id="x61h8cys">
:host {
color: red
}
</style>
<my-element>
<template shadowroot="open" adopted-styles="x61h8cys">
<!-- ... -->
</template>
</my-element>
In this wa x61h8cys
can be made available for other shadow roots without having to penetrate otherwise encapsulated DOM structures? Or do you think that all style[type="adopted-css"]
would be made available globally by default?
How would you make these "declarative CSS modules" available in an imperative context? Should there be API to allow for resolution of the actual Stylesheet object from the x61h8cys
identifier so that it can be adopted by shadow roots deeper within the DOM hierarchy or that might not be delivered declaratively?
Inversely, should stylesheets that are registered imperatively have API added by which to associate an ID (a la adopted-styles="x61h8cys"
) so that they could be leveraged in declarative code?
The example of
<script type="css" specifier="/foo.css">
:host {
color: red
}
</script>
Seems like a large change for developers, would it make a shorter step from historic practices to apply this concept via <style>
or <link>
? E.G:
<style type="module" src="/foo.css">
:host {
color: red
}
</style>
OR
<link rel="stylesheet-module" href="/foo.css" />
In this way we could leverage already existing blocking mechanisms while informing the browser not to apply these styles until adopted. It's possible that link[rel="stylesheet-module"]
might fit right into the rel="modulepreload"
cache once assert { type: 'css' }
hit browsers allowing it to leverage even more pre-existing features.
Question that applies to both of these approaches, do you expect that values applied to adopted-styles
or css-modules
would resolve lazily? E.G. could I list these style in my declarative shadow DOM but then load those sheets in a non-blocking way and still have them apply to the shadow root?
+1, I think the idea of using an attribute to provide a URL / specifier is a really interesting thing to explore because it seems like it would generalize well to basically anything in HTML that describes some other resource and has either inline and external forms. (I'm writing it here as cacheas
since I don't think it would really be a specifier anymore? Anyway, the name would need bikeshedding.)
So, not only could you have:
<script type="module" cacheas="./some-path">
export let aString = "Hello, World!"
</script>
<script type="module">
import {aString} from "./some-path";
console.log(aString);
</script>
and
<style cacheas="./some-path">
body::before {
content: 'Hello, World!';
}
</style>
<link rel="stylesheet" href="./some-path">
But also things like:
<img cacheas="./some-path" src='data:image/svg+xml,<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="120" height="60"><text y="30" x="30">Hello, World!</text></svg>'>
<img src="./some-path">
@mfreed7 if we wanted this to be a more general feature, should we move the issue to HTML?
Am I reading right that this idea is a form of declarative module blocks? A bit off-topic, but I'm wondering how that could play with import assertions and this concept, seems like a lot of the semantics are the same …
Yeah, URL based namespacing is exactly like JS modules. In theory, you should be able to define an inline (e.g. data URI) style inside import maps. So the only additional mechanism needed here is the ability to adopt it.
How about <style type="module" src="foo"></style>
.
@rniwa Would that <style>
be resolved synchronously? I just see the word "module" and think async, but this needs to be sync to avoid FOUC.
cc @mfreed7, @justinfagnani what do you think of the idea @rniwa is proposing here?
@justinfagnani , why to limit the adopted-styles
concept to the CSS only?
It is a "library" concept with common scope as in CSS as in custom components registry and perhaps some other common things like import maps.
I.e. this proposal has to be converted to something like "custom elements library scope".
If agree, the name adopted-styles
does not reflect the semantic meaning.
@matthewp @rniwa that seems good to me.
My main ask is for a way to emit a style module at the first use site when streaming server rendering a large tree of components, so that further components can use that same style sheet.
This is needed because some modern streaming SSR approaches do not know what components will be rendered ahead of time, due to request-specific data and data-dependent templating.
My other thought is whether this is a thing that should apply to all types of module scripts - where an inline tag can contain content, but populate the module cache.
I would prefer a syntax on the <template shadowroot>
rather than a <style>
inside the template. The issue for us (LWC) is that we use adopted stylesheets for pure client-side rendering (for perf reasons), whereas for SSR, since there is no equivalent, we emit an inline <style>
which has to be specially handled on the client.
The main perf benefit of adopted stylesheets that I've seen in my testing is just bypassing the DOM and avoiding the cost of creating/inserting a <style>
element (since browsers have optimizations for <style>
s inside shadow roots). So if we need to insert a <style>
element to match SSR, then I predict the perf benefits of adopted stylesheets would go away, and you may as well use an old-school inline <style>
.
Update: after discussion, https://github.com/WICG/webcomponents/issues/939#issuecomment-900791467 seems good to me. In SSG (non-streaming) at least, you can hoist the first <script>
up into the <head>
or something, which resolves the issue with the asymmetry between client-rendered versus server-rendered DOM structure.
WCCG had their spring F2F in which this was discussed. You can read the full notes of the discussion (https://github.com/WICG/webcomponents/issues/978#issuecomment-1516897276) in which this was discussed, heading entitled "Declarative Adopted Stylesheets".
In the discussion, present members of WCCG reached a consensus that this needs further discussion with implementers.
I'd like to point out that representatives of Apple, Google, and Mozilla were present in the meeting, but no firmer conclusion was made. This issue will likely require implementer feedback.
I'm not sure if this is the right place or if it is more related to #909 (open-stylable). But I would hope that declarative CSS module scripts would also automatically apply to the global document. Like I said in the other issue - most of the time we want to use CSS frameworks like Bootstrap or TailwindCSS and they need to be applied to the global document.
For https://github.com/webtides/element-js/blob/main/src/StyledElement.js we are currently duplicating inlined styles to make them adoptable.
<html>
<head>
<style id="globalStyles">/* all of TailwindCSS... */</style>
</head>
...
const globalStyles = document.getElementById('globalStyles');
class MyElement extends HTMLElement {
constructor() {
const shadow = this.attachShadow({ mode: 'open' });
const sheet = new CSSStyleSheet();
sheet.replaceSync(globalStyles.textContent);
shadow.adoptedStyleSheets = [sheet];
}
}
I hope that it will be possible to share styles between the global document and shadow roots instead of duplicating them.
This is the only thing stopping me to build a perfect solution. It would be great if we had something like:
index.html
<link as="style" rel="modulepreload" href="./app.css">
....
<style type="module" href="./app.css">
app.js (when not loaded in main html and dynamically loaded, we want to lazy get the css file)
import('./app.css', { with: { type: 'css' } })
.then(module => {
this.shadowRoot.adoptedStyleSheets = [module.default];
});
index.html
<style type="module" id="styles-app">
:host {
background-color: red;
}
</style>
app.ts (when it is in the dom, we want to add it to the adoptedStyleSheet)
this.shadowRoot.adoptedStyleSheets = [document.getElementById('styles-app').content];
That way the DOM parsing would be blocked until the CSS loads.
I like the css-modules
and specifier
approach. It works very well with how one would want to SSR a web component. If css-modules
could also support the @sheets
idea that would pretty much sum up what I'm looking for.
For example, common, rarely changing CSS could be multiple sheets in a single, cacheable file, while individual component css could be inlined. You might have something like this:
<link rel="stylesheet" href="design-system.css" specifier="/design-system.css">
<my-element>
<template shadowrootmode="open" css-modules="/design-system.css#typography /my-element.css">
<script type="css" specifier="/my-element.css">
:host {
color: red
}
</script>
<!-- ... -->
</template>
</my-element>
<my-element>
<template shadowrootmode="open" css-modules="/design-system.css#typography /my-element.css">
<!-- ... -->
</template>
</my-element>
And in JS you could also do this:
import { typography } from "./design-system.css" with { type: "css" };
import styles from "./my-element.css" with { type: "css" };
export class MyElement extends HTMLElement {
constructor() {
super();
// Elided: skip if DSD already exists.
this.attachShadow({ mode: "open" }).adoptedStyleSheets.push(typography, styles);
}
}
customElements.define("my-element", MyElement);
This should also work well for component authors who want to distribute their entire design system, providing a single, external CSS file that their customers can easily link, edit, or replace, without component code alterations needed.
Can someone share the use cases for this thing?
I understand the theoretical need for a serializable form of adopted stylesheets, but I have many concerns with the direction this is going.
What exactly does this provide that a regular <link rel="stylesheet">
inside <template>
doesn't? Is it just for reducing repetition of inline <style>
s (which could also be done using a data-uri, as @rniwa mentioned earlier)?
Off the top of my head, this provides:
I believe some use cases have already been listed, but:
@EisenbergEffect I appreciate your comment, but most of these things should be covered by <link rel="stylesheet">
and @sheet
(which can be used together with fragment URLs, according to this resolution), and some of the points feel subjective and abstract. Today, <link rel="stylesheet">
is already a performant way to handle styling in DSD, which is why I'm asking specifically what this proposal adds on top of it.
You mentioned modules, but this proposal doesn't really address that (hence the name "CSS module scripts"). True CSS modules would need to be usable within CSS itself, even before HTML or JS is involved. I'd imagine that CSS modules would allow a @sheet
to be @import
ed into another CSS file and applied there.
It's also worth pointing out that DSD still requires repetition of the shadow markup today. When we get HTML modules (and/or DCE), it would reduce this markup repetition. Since <style>
/<link>
is just markup, HTML modules basically solve this problem of repeated styles. This is also why I think it makes more sense to pursue HTML modules first.
Lastly, I want to highlight https://github.com/w3c/csswg-drafts/issues/10013, which was recently greenlit. It may sound unrelated, but in my mind it opens up the potential for these two syntaxes to refer to the exact same stylesheet instance:
<link rel="stylesheet" href="design-system.css#button">
import buttonStyles from "design-system.css#button" with { type: "css" };
The main purpose of this propose is the same as declarative shadow DOM: to be able to accurately serialize a valid DOM tree to HTML.
Right now there's no way to properly serialize adoptedStyleSheets. During SSR you can convert them to <style>
tags, but you can't deduplicate them, so you end up with potentially hundreds or thousands of duplicate stylesheets. Converting the to <link>
tags would likewise result in potentially hundreds of HTTP requests. Both of those workarounds break the semantics of constructible stylesheets.
The <style>
and <link>
tags may be duplicated in the HTML, but they will be optimized by all browsers to point to a single stylesheet instance (similar to adopting a constructed stylesheet). And the HTML size is usually not an issue because it compresses well (and will be completely deduplicated with HTML modules).
It's still non-optimal and breaks semantics. Constructible stylesheets actually share a single stylesheet instance, something that doesn't happen with <style>
or <link>
.
There is still a performance hit from so many <style>
tags even with deduplication - this was part of the motivation for constructible stylesheets in the first place. The HTML parser still has to consume the bytes of each tag and check the cache. There's still an extra element in the tree. And compression doesn't completely make up for the extra bytes.
<link>
tags would be pretty cumbersome to implement in an SSR system because you have to invent a URL scheme for them. It would be ok with CSS modules, but won't work well for constructible stylesheets created from inline CSS.
Lastly, I want to highlight w3c/csswg-drafts#10013, which was recently greenlit. It may sound unrelated, but in my mind it opens up the potential for these two syntaxes to refer to the exact same stylesheet instance:
<link rel="stylesheet" href="design-system.css#button">
import buttonStyles from "design-system.css#button" with { type: "css" };
@mayank99 has there explicitly been discussion to date that implied that files included via rel="stylesheet"
will make it onto the module graph so that they are not requested multiple times. I'd love to join that conversation! The way that opens an alternate version of the above where in a file full of @sheet
fragments could be included via rel="stylesheet"
:
<link rel="stylesheet" href="design-system.css">
While being added to the module graph so that extended usage of the same in JS would essentially be modulepreload
ed for use like:
import { button as buttonStyles } from "design-system.css" with { type: "css" };
import element from '/my-element.css' with { type: 'css' };
class El extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.adoptedStyleSheets = [buttonStyles, elementStyles];
}
}
Would be awesome!
Passing that same theoretical approach to developers using DSD would be important to ensure that they can share learned concepts between the HTML-first or JS-first implementations of a shadow root.
<my-element>
<template shadowrootmode="open" adoped-style-sheets="/design-system.css#button /my-element.css">
<!-- ... -->
</template>
</my-element>
Expanding this to include inline <style>
elements completes the picture, no matter how you prefer to write you application:
<link rel="stylesheet" href="design-system.css">
<style type="module" href="/my-element.css">
// styles
</style>
<my-element>
<template shadowrootmode="open" adoped-style-sheets="/design-system.css#button /my-element.css">
<!-- ... -->
</template>
</my-element>
<script type="module">
import { button as buttonStyles } from "design-system.css" with { type: "css" };
import element from '/my-element.css' with { type: 'css' };
class El extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.adoptedStyleSheets = [buttonStyles, elementStyles];
}
}
</script>
😍
Editors note: APIs and attribute name FPO only!
has there explicitly been discussion to date that implied that files included via
rel="stylesheet"
will make it onto the module graph so that they are not requested multiple times.
@Westbrook Not yet, but that's one of the things I'm hoping will be discussed in CSSWG when we talk about adoptable non-constructed stylesheets and @sheet
and such. I think these discussions need to happen in parallel with (if not before) declarative adopted stylesheets.
Declarative shadow roots and constructible stylesheets need to be made to work together so that stylesheets added to shadow roots can be round-tripped through HTML serialization and deserialization.
As of now, SSR code will have to read a shadow root's adopted stylesheets and write them to HTML as
<style>
tags. While this will style they shadow roots correctly on first render, it results in bloated a HTML payload and breaks the shared stylehseet semantics. Hydration code would have to deduplicate<style>
tags and create and attach constructed stylesheets in order to reconstruct the origin DOM structure.To solve this we need a serialized for of constructible stylesheets. These are stylesheets that do not automatically apply to any scope, but can be associated and applied by reference.
Requirements:
One idea is to add a new type for
<style>
tags that accepts CSS text and creates a new constructed stylesheet:This style can then be associated with a declarative shadow root:
Having a type other than text/css means that the styles won't apply to the document scope even in older browsers. It's also what allows it to have an adoptable CSSStyleSheet .sheet, which replace() works on as well.
One problem that immediately come up is scopes and idrefs. If a declarative stylesheet is only addressable from within a single scope, it's no better than a plain
<style>
tag because it would have to be duplicated in every scope that uses it. We need some sort of cross-scope idref mechanism. With that we can write the styles with the first scope that uses them, and reference them in later scopes in the document:Here
xid
is a stand-in for some kind of cross-scope ID."x61h8cys"
is a stand-in for a GUID or other globally unique value (such as a hash of the style text) that the SSR code would be responsible for generating.There are other instances where we need cross-scope references, like ARIA and label/input associations. I'm not sure if these cases are similar enough to use the same mechanism or not.