With Cascading Stylesheet Module scripts on the horizon we will soon have the ability to import CSSStyleSheets directly into JS, like so:
import sheet from './styles.css' assert {type: 'css'};
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'};
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.
@sheetTo 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:
@sheet sheet1 {
:host {
display: block;
background: red;
}
}
@sheet sheet2 {
p {
color: blue;
}
}
These could be imported separately from JS:
import {sheet1, sheet2} from './styles1and2.css' assert {type: 'css'};
And also be available on the main style sheet:
import styles, {sheet1, sheet2} from './styles1and2.css' assert {type: 'css'};
styles.sheet1 === sheet1;
styles.sheet2 === sheet2;
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:
import {sheet1, sheet2} from './styles.css' assert {type: 'css' };
// doesn't yet exist, but a utility that re-writes class selectors and returns
// an object with a .sheet property and properties for each class
import {scopeSheet} from 'css-module-utilities';
const scopedSheet1 = scopeSheet(sheet1);
const scopedSheet2 = scopeSheet(sheet2);
document.adoptedStyleSheets.push(scopedSheet1.sheet, scopedSheet2.sheet);
document.append(`<div class="${scopedSheet1.fooClass}"></div>`);
document.append(`<div class="${scopedSheet2.barClass}"></div>`);
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.
Most helpful comment
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.