Open justinfagnani opened 3 years ago
cc @dandclark @yuzhehan
Early thoughts: I like this, though it might not replace CSS bundlers in some really performance-sensitive cases. There is still one fetch incurred for the import {sheet1, sheet1} from './styles1and2.css' assert {type: 'css'};
statement that would be eliminated by bundling. If the perf benefit of eliminating this last extra fetch is greater than the perf benefit of parsing everything directly as CSS [1], then there might not be a performance win for using this instead of a bundler.
But, it reduces the cost of using CSS modules in production to just 1 extra fetch, which is down from N extra fetches for N stylesheets. So for the cost of the one fetch, you cut out one part of the build/bundling process, get some perf benefit from parsing CSS directly without passing it through the JS parser, and the resulting production code will be easier to read and reason about than production code that had CSS bundled in the JS.
[1] Last year I did some rough experiments to try to measure this potential perf benefit. I observed real differences in both time and memory, although you need a lot of iterations before it starts to be really observable: https://dandclark.github.io/json-css-module-notes/#css-module-performancememory-examples
If there is a way to add this to the polyfill I would be more than happy to include this in the SystemJS module types polyfill as well. It seems a great feature.
@justinfagnani A small typo correction for clarity: you've got {sheet1, sheet1}
in a few places where I think it should be {sheet1, sheet2}
.
Thanks @dandclark! Updated
The semantics here are fine for unbundled apps, but bundling becomes tricky. If you have two .css files in an app, you can't just combine them. ie:
import sheet1 from './styles1.css' assert {type: 'css'}; import sheet2 from './styles2.css' assert {type: 'css'};
Is not compatible with:
import sheet from './styles1and2.css' assert {type: 'css'};
Dumb question from someone lacking context about CSS modules: these aren't compatible because sheet1 and sheet2 are both CSSStyleSheet
objects consisting of their respective parts, right? Hence you need some way to split up the concatenated stylesheet back up into its constituent parts?
@gsnedders correct
This sounds fairly reasonable to me. Semantically, this is basically a more convenient way to write @import url("data:...");
, with the potential to hook into CSS Modules a little better.
cc @bmeck this seems like a good addition for the arbitrary module names, if my bundling still want to reference the names somehow.
I like this as it also helps on a better usage of dynamic import()
for css files, IMO.
This proposal is very interesting to the Parcel team, and seems to mirror the Module Fragments proposal in the JavaScript world. Current approaches to bundling CSS by simply concatenating them are prone to ordering issues that can cause specificity problems. Sometimes bundling is simply not possible to do correctly due to this. An approach that allows natively combining separate sheets together that can be applied in the correct order would be very useful. 👍
cc'ing @littledan who is working on the related module declarations. If language-specific bundling is needed despite web bundles, maybe we need to push this forward for CSS.
It turns out you can emulate this feature by abusing @supports
.
If there's an unknown supports function, the parsed stylesheet will still contain the rules inside @supports {...}
. We can invent a function like sheet(name)
and stash rules in there keyed by the bundled sheet name:
@supports sheet(styles-one.css) {
//...
}
@supports sheet(styles-two.css) {
//...
}
Then we can rewrite CSS module script imports from :
import styles from './styles-one.css' assert {type: 'css'};
to:
import $bundledStyles from './styles-bundle.css' assert {type: 'css'};
import {getBundledSheet} from 'lit/get-bundled-sheet.js';
const styles = getBundledSheet($bundledStyles, 'styles-one.css');
where getBundledSheet
is a utility to grab a set of rules in a @supports sheet(name)
at-rule and create a new stylesheet out of them.
Proof of concept here: https://lit.dev/playground/#gist=5fab7cc0987e6f1610ba3bd4f432f02c (requires import assertions support to work)
@tabatkins adding native support for something like @sheet
would eliminate the processing and double-parse (bouncing rules though .cssText
and insertRule()
). Seems relatively simple (but famous last words). What do you think about adding this?
This seems like a good progression using assertions. Bundles would become much more useful from CSS perspective.
/* bundle.css */
@layer defaults, common, brand, components;
@sheet designsystemstyles {
@layer commmon {
...
}
}
@sheet nameofbrand {
@layer brand {
...
}
}
As I said in my previous comment, I think it's a very reasonable suggestion, I just haven't spent any time speccing it out. ^_^
A few questions that probably need resolving, tho:
@sheet
rules, yeah? We just additionally specify that the @sheet
rules produce additional exported sheets in the module object, keyed to their name?@sheet
contents are independent, as if they were @import url("data:...");
, right? The example in the preceding comment would indeed work (layer names are shared across all sheets already) but it wouldn't, say, see a @namespace
rule in the outer sheet (and presumably could contain a @namespace
rule of its own).@media (...) { @sheet {...} }
? I presume the answer needs to be "it's invalid", and @sheet
objects are required to be top-level.@sheet
s tho? If so, does the top-level import expose them all as a flat list, or do you just get the top-level ones, and have to go find the lower ones yourself?@sheet foo
rules, do we only take one and treat the rest as invalid (and then do we take first or last?)? Or do we merge their contents as if they were a single sheet?@import @sheet sheetName
in order to share across the single file, the goal is to keep the various @sheet
entries separate from each other, but bound to the single file download.@sheet
should be top level.@sheet
should be top level.const
s throw errors. CSS doesn't really throw errors, so keep towards the CSS rules here.1 and 4 in concert beg the question of what is returned at import styles from './styles.css' assert { type: 'css' };
when the file does have @sheet
specifiers? In the JS space, you'd hope for something more of an object with the other sheets on in { sheet1: CSSStyleSheet, sheet2: CSSStyleSheet, ...etc }
, however, I'd expect we'd need to actually accept a CSSStyleSheet with its cssRules
array including CSSStyleSheet
s, instead of rules. This could be a bit surprising to JS users, but clarifies the fact that 4 is not possible. If there were some magic way to get more of a JS import out of the the CSS assertion, it would be interesting to push for nested sheets to do the same, but both feel a bridge to far in a world where we'd actually want something like this to ship...
I presume the top-level sheet is the default export, and @sheet
s just provide named exports, so you can get either or both, depending on which syntax you use for the import.
Shouldn’t user-defined sheet (and layer) names be either quoted strings or dash-idents?
No, not necessarily. They're user-controlled, so there's no need for strings (they can comfortably remain in the identifier syntax). And these names won't mix with other CSS values, so there's no particular need to mark them out as user-defined; plus they're meant to map to JS identifiers, so dashed-idents would be inconvenient.
If the goal is bundling files that otherwise would be part of the module graph, I think some of the questions you raised @tabatkins seem to me to have relative obvious answers, and I would agree with @Westbrook's responses.
The main thing for me is that the module graph is keyed by URL, and so is flat in that sense, so @sheet
should be top-level only. References between sheets, should that become possible in the future, should be via a module-graph compatible @import
that could reference other @sheet
s.
That makes sense. 👍
Agenda+ to check the group's temperature on this idea (and figure out what spec it would go in).
To sum up:
The problem to solve is bundling stylesheets together. (Particularly for importing in a JS import, but also more generally to consolidate multiple requests into fewer, larger requests.) There are some inconvenient workarounds today (@import url("data:...");
, a false @media {...}
rule that you then reparse the contents of yourself, etc), but they suck, and also don't interact nicely with JS imports.
@sheet <sheet-name (ident)> {<stylesheet>}
rule, which can contain anything that can go in a top-level sheet. (Except @charset
, but that's not really a rule anyway, just a weird encoding flag handled before parsing happens.) So for example, @sheet
can contain @namespace
, etc.
@sheet
are treated as an independent stylesheet, as if they were linked in via an @import url("data:...");
.@sheet
rules have the same placement restrictions as @import
- only at the top of a styelsheet.@sheet
is only allowed at the top level of a stylesheet (no nesting in @media
, etc), and also can't be nested inside of itself.@sheet
rules have the same name, all but the last is ignored.@sheet
rules are exposed as separate CSSStyleSheet objects as named exports.So for example, given a stylesheet like:
@sheet one { .bar {...} }
@sheet two { .baz {...} }
.foo {...}
then JS can import the sheet like:
import topLevel, {one, two} from "./combinedSheet.css";
Remaining design questions:
@sheet
is meant to emulate an @import
with a data url, should we let it take the same import restrictions - a MQ, a SQ, a layer? Or is it okay to expect these to be translated into rules inside the @sheet
wrapping everything?Great summary @tabatkins !
When importing a stylesheet using JS imports, while the overall sheet is the default import (how it works today), all the @sheet rules are exposed as separate CSSStyleSheet objects as named exports.
I do want to be careful and conservative about this point, since it would be the first introduction of named exports in a stylesheet module. How might it interact with some of the ideas in https://github.com/w3c/csswg-drafts/issues/3714 - that might create named exports for things like class names like userland modules do today.
I don't think it's much of a concern, since it might be rare to have top-level rules/exports and bundled stylesheets, and even if you did you might be very unlikely to have name collisions, and even if you did have collisions, you're probably generating a bundle with a tool that can handle that by renaming things. But it's something to consider.
fwiw, one reason to expedite this feature would be that if it lands by the time all browsers support import assertions and CSS module scripts, then we might not need to handle cases where browsers support CSS modules but not nested stylesheets. That would simplify using CSS modules scripts in production a lot.
This may be closely related to @justinfagnani’s comments: it would feel strange to add named @sheet
to CSS just so that JS works better, therefore I would expect that CSS’s own @import
could make use of sheets to only import select parts of other stylesheets.
Furthermore, could named sheets be exposed as URL fragments then, e.g. <link rel=stylesheet href="style.css#sheet">
?
Also, I still like the idea of namespaced custom properties inside variable references from #6099:
@import CD from url(corporate-design.css);
a {color: var(-CD-link-color);}
Again, we've historically preserved the single-dash idents for vendor prefixes, and there's no good reason for us to invade that space now. Please stop suggesting it.
Furthermore, could named sheets be exposed as URL fragments then, e.g. ?
Hm, this could be interesting actually. @justinfagnani, how do you feel about that? On the JS side it would still be multiple import
statements, but only one request (assuming caching semantics allow reuse of the resource).
Furthermore, could named sheets be exposed as URL fragments then, e.g. ?
Hm, this could be interesting actually. @justinfagnani, how do you feel about that? On the JS side it would still be multiple import statements, but only one request (assuming caching semantics allow reuse of the resource).
This sounds really smart to me. It could work as <link rel=stylesheet href="style.css#sheet">
's inside shadow roots too.
Features that broaden the scope to as many style loading techniques and frameworks as possible sound great to me.
@import
(or something similar) between bundled @sheet
would be great too, though that may require settling on a semantic for @import
in CSS module scripts.
As mentioned above, I think it's pretty important that a feature this invasive has a CSS use apart from any JS. On the surface, I like the idea of allowing @import
/link to access fragments of a larger css file – but I'm curious what the use-case is for that. My immediate guess would be performance optimization, but I'm not actually sure it helps there? Now we're loading a large file, when we only want access to one part of it. But maybe it helps cut down on parse/render time? Is there some other story for how this helps CSS authors write more modular CSS, without relying on JS for the feature to do anything?
Regarding potential use cases for this feature, this could be very useful in styling things like declarative shadow DOM, especially if there's ever a mechanism for non-constructed stylesheets to be applied to adoptedStyleSheets
.
For a CSS only usage, I'd say leaning into something like <link rel=stylesheet href="style.css#sheet">
would be one path, though more invasive. This allows the download of all of the CSS that might be needed for an experience in one request without leaving a bunch of unused CSS attached to the page (performance hit) until you need it. Even if the "need it" doesn't come till the loading of a subsequent page, you'll now have a faster load time as the file is cached from the previous request.
@calebdwilliams also hits this on the head for the benefits it brings to declarative shadow DOM. Having a path for a file with multiple @sheet
s to be including once and then the declarative shadow roots to be able to take parts of that would be a great benefit to that process. This likely requires a little more invasion as it would benefit from something along the lines of https://github.com/WICG/webcomponents/issues/939 as well as ensuring that all CSSStylesSheets were "constructible stylesheets". Each of these could then be addressed later (regardless of their initial acquisition) while also allowing for the form of sub-querying that @sheet
s would open up a lot of options around how these styles could be used.
The CSS Working Group just discussed Multiple stylesheets per file
, and agreed to the following:
RESOLVED: Accept @sheet with URL fragment referencing rule. Exact details to be in the Cascade spec
As mentioned above, I think it's pretty important that a feature this invasive has a CSS use apart from any JS.
If there were an accompanying "apply this sheet here" @rule, it could effectively become a "mixin."
@sheet redMixin {
:scope {
background: red;
}
}
.myContainer {
@apply-sheet redMixin;
}
For full mixin functionality, you would want the ability to pass in arguments.
I believe that this feature also provides a way to handle a recent set of community requests that have surfaced around component customization.
If Web Component library authors distribute their CSS in a single file with @sheet
s for each component, then it will make it very easy for component consumers to provide custom styles or override styles of the 3rd party components. They simply replace the external CSS file provided by the library with their own. Import maps can be used if needed.
I’m wondering if @export
would be a name more consistent with the rest of the web platform. I was confused when I first saw @sheet
, but at least to those who write JS, export
is a familiar concept. We already have @import
, so this would be nicely analogous.
@LeaVerou I would think that @export
should be used for a more general export mechanism that might export selectors or class names, rulesets, upcoming mixins and functions, etc. This feature is for defining an independent nested stylesheet ,so @sheet
describes what it is, and they're just exported implicitly because they're not useful when not exported.
If a sheet should be explicitly exported, maybe the syntax should be @export @sheet
?
@export @sheet sheet2 {
p {
color: blue;
}
}
I'm not sure if @-rules can work that way though.
@justinfagnani I’m not sure what a mechanism to export selectors or class names would look like. Rulesets, mixins, functions can already be exported with this rule since its contents would be <stylesheet>
, no? From an architectural pov, I would be wary of introducing features in the web platform to do things that are seen as similar but slightly different, as that tends to cause confusion and poor DX.
From an architectural pov, I would be wary of introducing features in the web platform to do things that are seen as similar but slightly different, as that tends to cause confusion and poor DX.
This is a worthwhile line of investigation, and would likely benefit from actual user testing or surveying, etc. Historically, we've done import
/export
within the JS context, but neither CSS nor HTML have done this bi-directionally, even in their own context, to the best of my understanding. As we expand the use of Import Attributes, it'll be good to take a stance on whether that should be more like something that exists or more clearly a new feature. That hopefully means that an answer here could be applied to the conversation around with { type: 'html' }
, too.
With this in mind, I wonder if the use of @export
would be more confusing as it is not reciprocal to the @import
token that is already available in CSS?
@LeaVerou
I’m not sure what a mechanism to export selectors or class names would look like
Some ideas are in here, though without the @export
keyword: https://github.com/w3c/csswg-drafts/issues/3714
Rulesets, mixins, functions can already be exported with this rule since its contents would be
, no?
What if you wanted to import a single mixin and apply it in a specific place, not a whole stylesheet?
Does this feature open up the path to real "CSS modules"?
Right now, all CSS imports are side-effectful, but @sheet
could make it possible to split the "import" vs "apply" parts into two steps.
/* In foo.css */
@sheet A {…}
@sheet B {…}
@import "foo.css" sheets(A, B);
@sheet B;
@sheet A;
Also it would be useful to be able to rename sheets when importing. I think this functionality is essential for any module-like system.
@import "foo.css" sheets(A as X, B as Y);
@sheet X;
@sheet Y;
Lastly — and this might be far-fetched — maybe there should be some reserved sheet names that have special meaning. For example, @sheet inherit;
could be used to reference host context stylesheets from within a shadow-root. This could be a solution for open-styleable.
<head>
<style>
@sheet A {…}
@sheet B {…}
</style>
</head>
<div>
<template shadowrootmode="open">
<style type="module">
@sheet inherit.A;
@sheet inherit.B;
/* or just @sheet inherit; */
</style>
</template>
</div>
Regardless of the specific ideas and syntaxes I illustrated, I think we should all be thinking about how to make CSS (and HTML) better and more useful on its own, even when JS is not involved.
Right now, all CSS imports are side-effectful
CSS imports only return a CSSStyleSheet object - they don't have any side-effects.
I'm talking about CSS @import
s (in CSS), rather than CSS module script import
s (in JS).
With this in mind, I wonder if the use of @export would be more confusing as it is not reciprocal to the @import token that is already available in CSS?
100% agree - I would naturally assume I could @import
something I have @export
ed, although from the CSSWG meeting transcript it looks like you will be able to to do this so maybe the export name is ok (although I've not seen anyone answer Miriam's question about whether being able to @import
an @export
is even a useful thing to be able to do, whereas the JavaScript use case is very compelling by itself).
Because the thing getting exported is a CSSStyleSheet
I find the @sheet
syntax more clear regardless.
@export @sheet
This syntax is ugly imo. Particularly if you are using other @rules
like @layer
within the sheet, its too many @
s that it feels a bit overwhelming. It would be unfortunate to adopt an unnecessarily verbose syntax just for the purposes of catering to a totally speculative potential future CSS feature.
In my previous comment, I described a CSS way to import an existing @sheet
from another file.
Another useful thing would be the ability to name a regular stylesheet from HTML, using an attribute like name
(similar to <link title="…">
).
<link rel="stylesheet" href="legacy.css" name="legacy" />
This would give you a @sheet
named legacy
can be referenced elsewhere.
With Cascading Stylesheet Module scripts on the horizon we will soon have the ability to import CSSStyleSheets directly into JS, like so:
Problem
The semantics here are fine for unbundled apps, but bundling becomes tricky. If you have two .css files in an app, you can't just combine them. ie:
Is not compatible with:
The current workaround is to compile CSS into JS modules, which defeats some of the performance benefit of having the browser directly load and parse CSS.
Web Bundles might solve this problem generically for multiple file types, though its future on multiple browsers seems unclear right now.
Proposal:
@sheet
To fix this and allow bundling of CSS, could we introduce an at-rule that contains an entire style sheet as its contents?
For example, there could be a
@sheet
rule which allows files to contain named stylesheets:styles1and2.css:
These could be imported separately from JS:
And also be available on the main style sheet:
Relation to existing approaches
The proposal is most obviously relevant to code that manages CSSStyleSheets in JS - ie, users of Constructible StyleSheets and the API currently named adoptedStyleSheets.
It would also be useful as a bridge to userland CSS loaders that do bundling and scoping via selector rewriting. By standardizing bundling, scoping could be done with client-side utilities: