whatwg / meta

Discussions and issues without a logical home
Creative Commons Zero v1.0 Universal
93 stars 161 forks source link

Allow authors to apply new css features (like cascade layers) while linking stylesheets #240

Closed mirisuzanne closed 2 years ago

mirisuzanne commented 2 years ago

The CSSWG recently added a feature to CSS called Cascade Layers. It allows authors to more explicitly manage the cascade behavior of styles from different parts of a design system. That can be done using @layer blocks in the code, but in many cases authors will want to wrap entire (sometimes third-party) stylesheets into layers at the time they are linked/imported. The new CSS spec makes that possible with additinoal @import syntax - but the @import rules tends to be less performant than the HTML <link> tag. For now, the best authors can do is use @import inside HTML <style>:

<style>
@import url(reset.css) layer(reset);
@import url(default.css) layer(default);
@import url(bootstrap.css) layer(framework);
@import url(theme.css) layer(theme);
@import url(utilities.css) layer(utilities);
</style>

Use cases

A few variations on the layer use-case include:

Cascade Layers are already supported in preview/beta versions of Safari, Chrome, and Firefox - and we expect them to appear in stable releases over the next few months. But the lack of HTML link support has been one of the largest points of concern/feedback from authors, and one that we can't address from the CSSWG.

Constraints

At first glance it may seem like this could be solved with a new layer attribute on the <link> tag, but it would cause problems for old browsers that simply ignore the attribute, and continue to the load the stylesheets without any layering. Whatever solution we land on needs to allow authors more control over the fallback path for old browsers.

It might also be good to plan for this sort of situation down the road, if we're able to find a solution that can potentially be used again for new features in the future?

(I'm sure I missed some useful information here, so feel free to ask questions!)

nachtfunke commented 2 years ago

I wanna add that I believe this is absolutely crucial for the adoption of Cascade Layers. Authors are conditioned to avoid the @import syntax for performance reasons (example A, example B). It is already possible to conditionally link stylesheets based on media type/media query, being also able to also define a layer makes this also more congruent to their definition:

Authors can create layers to represent element defaults, third-party libraries, themes, components, overrides, and other styling concerns—and are able to re-order the cascade of layers in an explicit way, without altering selectors or specificity within each layer, or relying on source-order to resolve conflicts across layers.

Stylesheet authors may not be able to use @import at all, or they might not be able to change how a different external stylesheet enters the cascade all together, either because they don't have access to the processes that provide the stylesheets, or because the created stylesheet just doesn't exist to be imported, because it is being created as part of an obfuscated, optimised build-process, that will end up linking stylesheets in an automated process as well.

As the order of the cascade layers can be declared in the stylesheet that authors can author, according to spec, this would allow authors to clearly separate incoming, external rulesets.

This is important, because most authors currently will rely on a preprocessor such as SASS to split code into partials because authors are engrained with negative connotation for @import's, as mentioned at the beginning. Because of that, switching to cascade layers will feel like a big cost that many authors might not want to pay. But they might be, if they can do it with <link>.


I personally also believe that there is a specific case to be made for refactoring sites that rely on multiple libraries. As approaches change with trends and needs, allowing Authors to assign existing, incoming stylesheets to a dedicated layer from where they are linked may allow authors to better differentiate between existing code and new code. Tooling will adapt to working with cascade layers, maybe even providing a visual support to aid with it.

adactio commented 2 years ago

I wonder if the as attribute could be repurposed for cascade layers?

Right now, as can only be used if the rel value of the link element is preload or prefetch, which frees it up for use on stylesheet.

<link rel="stylesheet" href="reset.css" as="reset">
<link rel="stylesheet" href="default.css" as="default">
<link rel="stylesheet" href="bootstrap.css" as="framework">
<link rel="stylesheet" href="theme.css" as="theme">
<link rel="stylesheet" href="utilities.css" as="utilities">

On the one hand, it seems handy to re-use an existing attribute rather than minting a new one.

On the other hand, it could be quite confusing for the same attribute to be used for two different purposes:

<link rel="stylesheet" href="bootstrap.css" as="framework">
<link rel="prefetch" href="https://fonts.googleapis.com/css?family=Roboto:400,600" as="style">
domenic commented 2 years ago

Heya, thanks for opening this. I appreciate the focus on use cases. Let's talk about how to solve them, and in particular how far we are from a solution in today's world.

From what I understand, the functionality is available today with <style> + @import. The problems with that are:

Is that right?

If so, the traditional next steps in this sort of situation would be to clarify the performance issue, since that seems to be at the root of most of the problems (except for the last?).

For that, I'd suggest benchmarks comparing a prototype implementation based on <link> (even if it has bad backward-compat issues and thus is not shippable) versus the current implementation based on <style> + @import. Alternately or additionally, we'd want browser performance and/or rendering engineers to chime in with whether they think the performance issue is a matter of optimization work, or is fundamental to the design. (My guess is if there is such a delta, it's not fundamental to the design---<style>@import url(x) layer(y)</style> seems like it should be easy to treat the same as <link rel="style" href="x" layer="y">---but I am not one of those types of browser engineers!)

For example, I know Chromium did some implementation work that was supposed to ensure that <style> + @import is as fast as <link>, at least with regards to the preload scanner. If that has closed the gap, it might mean this is just a matter of ensuring that all engines do similar work!

mirisuzanne commented 2 years ago

There might be some interesting advantages to encouraging more use of the <style> tag (and making it more performant) - since that would also allow authors to specify the layer-order easily up-front, making import order less important, and order more clear:

<style>
/* establish layer order */
@layer reset, default, theme, framework, utilities;

/* import styles into layers */
@import url(theme.css) layer(theme);
@import url(utilities.css) layer(utilities);
/* etc… */
</style>

I'm not sure which browser engineers to ping here for input on that approach, and the performance issues. In the CSSWG issue, @yoavweiss suggests that:

The Chromium/WebKit preload scanner for CSS imports is significantly more fragile than its HTML equivalent, as it doesn't perform "real" tokenization. It'd be better to not put the weight of this feature's performance on it. Also, Firefox doesn't have such a preload scanner AFAIK.


@adactio, an attribute solution (new or existing) would need to invalidate the entire import for browsers without layer support - otherwise the fallback path is very unpredictable. Would that be true with as?

I know there's also been suggestions of having a new <style src='url'> syntax, which would certainly allow us to add or re-use an attribute. I like the symmetry that has with script, while being similar to current link. If we went that rout, tho, I think we might want to consider how we plan for similar feature additions down the road. Can't pick a new element every time.

nachtfunke commented 2 years ago

Is that right?

That is right. I am sorry I couldn't form a usecase that makes sense enough or if it just didn't make much sense. Maybe I can draw up a more comprehensive case, and once I do, I will follow up here.

I am myself currently working on a projects that generates CSS without having any control over how it comes into the markup itself. The point I am trying to make is that we cannot assume that authors will @import external stylesheets. From the point of view of an Author, I see no reason for why I shouldn't be able to also define a layer when liking a stylesheet.

It just seems incongruent to limit this feature to the stylesheet itself. Existing projects may have several stylesheets linked in a given document (global declarations, font-face declarations, declarations dedicated to theming), reworking the architecture and maybe even build-processes may seem too costly to support this feature. But I might be able to assign these existing stylesheets to layers, so that I can more clearly separate their concerns, especially for future reworks.

Let me know if this makes a bit more sense to you. English is not my first language and I find it a bit difficult to describe this topic sufficiently enough so that it makes sense. Luckily I think the other points raised by @mirisuzanne are already strong enough on their own - at least I hope so! :)

domenic commented 2 years ago

I am myself currently working on a projects that generates CSS without having any control over how it comes into the markup itself.

In that case it seems like you could not use any new markup-based solution (e.g. <link rel="stylesheet" layer="...">). Correct? You can only use @import, since you can only control the generated CSS, not how it comes into the markup.

domenic commented 2 years ago

I'm not sure which browser engineers to ping here for input on that approach, and the performance issues. In the CSSWG issue, @yoavweiss suggests that:

Ah yeah, that is a great thing to bring up. My person from Gecko to ping for style issues is @emilio; for WebKit I think @smfr? Anyway, if such folks generally agree that the fragility/difficulty of tokenizing CSS (instead of HTML) in the preload scanner is significant enough that we'd want to invent a new markup pattern, then that's a very helpful signal.

The biggest costs I can think of a new markup pattern, beyond the usual costs of implementation/tests/developer outreach, are the security interactions. E.g. if people are using dumb HTML sanitizers that somehow the new markup pattern would bypass, that can be tricky. And we'd need to think through and test integrations with existing security primitives like CSP. These are not blockers, but just costs to be weighed.

an attribute solution (new or existing) would need to invalidate the entire import for browsers without layer support - otherwise the fallback path is very unpredictable. Would that be true with as?

Unfortunately no; as="" is currently ignored for rel="stylesheet", so using it would be the same as using a new element, and would not invalidate the import in existing browsers.

I know there's also been suggestions of having a new <style src='url'> syntax, which would certainly allow us to add or re-use an attribute. I like the symmetry that has with script, while being similar to current link. If we went that rout, tho, I think we might want to consider how we plan for similar feature additions down the road. Can't pick a new element every time.

Agreed, we should definitely solve the future-additions problem if we go that route. Any ideas in that direction would be welcome.

mirisuzanne commented 2 years ago

Agreed, we should definitely solve the future-additions problem if we go that route. Any ideas in that direction would be welcome.

One idea might be having one or two more generic attributes that get @import syntax, rather than splitting out individual attributes for each aspect of that syntax? That could go a few different ways:

While it's nice to have things split into more individually meaningful attributes, this seems like the clearest way to make it forward-proof, and allows authors to match syntax between the two types of importing.

nachtfunke commented 2 years ago

[...] since you can only control the generated CSS, not how it comes into the markup.

Yes that is correct, but - and maybe I am overthinking this - in that scenario, the tool that doesn't write the actual css but does split it up and does write the HTML code could also add layer="vendor" or something of the sorts to its links. I am not sure how this affects layer ordering, as the spec says:

Cascade layers are sorted by the order in which they first are declared

But example 31 also says:

The statement syntax allows establishing a layer order in advance, regardless of the order in which style rules are added to each layer.

The way that I see it, is that allowing tools which automatically add stylesheets via <link> elements to the document to also declare them as part of a layer, even if that layer name itself cannot be defined, could be valuable - maybe in a way we are not yet aware of.


I think I can provide two examples to make my case, though whether or not layers actually are a viable solution in these scenarios I can only determine hypothetically:

In the first example, I worked on a large dynamic website that was built on top of the Zend framework. Forms where generated using the Zend Form library. Now as soon as forms where used and client-side validation was set, it also added its own stylesheet (and javascript) to render validation error messages. We couldn't touch the file without essentially ripping the plugin out of its package manager, but zend did provide us with ways to define how stylesheets like these did converge in the final document (it's been a minute, but I think it was called headLink or something), which accepted a list of parameters. In that case, we could have declared a layer attribute on it, saving us lots of complex selectors to override them.

In the second example, which is more recent, I worked on a Vite powered project. Vite asserts full control over an index.html file and in the case of this project, it was crucial, that it also controlled how stylesheets where split, merged and linked with the document in the HTML. It didn't write any actual CSS, but it did process stylesheets from npm managed packages, which we can't just @import in css, but the build tool can process it. It did however make decisions that ended up affecting the styles based on whether it ran in production or development mode. In development, whatever order was defined by the authors was left intact, because it didn't fully process the referenced files. But in production, it did - and it also all of a sudden changed the source order of the stylesheets. It also merged two files together, I guess because it determined it to be better so - but the point is, that because of the changed source order, some page-specific styles didn't override global styles and some global styles didn't override vendor styles. With the capability of simply declaring a layer on a linked resource, the styles would have still behaved like defined. (Of course I realise that layers are not supposed to fix this issue, I am trying to say that having this capability might be relevant because build tools can use it)

emilio commented 2 years ago

Firefox definitely has a preload scanner of sorts, fwiw, and we do scan for @import rules: https://searchfox.org/mozilla-central/rev/d4b9c457db637fde655592d9e2048939b7ab2854/layout/style/ImportScanner.h

You can already put ~whatever at the right of an import rule, and I think right now we'd preload it. It's of course less precise than <link media= etc where we can discard non-matching media queries and so on, but I don't think that should matter for layer()?

So we can just ignore @layer before @import like we do for @charset and we'll preload the right uris afaict.

emilio commented 2 years ago

I filed https://bugzilla.mozilla.org/show_bug.cgi?id=1750917 for that fwiw.

tabatkins commented 2 years ago

So, in https://github.com/w3c/csswg-drafts/issues/2463 we just resolved to add @supports at-rule(...) to test for support of a given at-rule.

I don't know if <link> can currently take supports queries, but if it doesn't, I suspect we could just extend media= to take the same syntax as the @import rule, where you can use a supports() function that takes a supports query, alongside the naked MQ syntax.

This is layering unimplemented features on top of unimplemented features, which is normally a problem, but in this case it's fine for things to fail if they're not understood or if they're understood but @layer isn't supported, and so that should work I believe.

mirisuzanne commented 2 years ago

@tabatkins are you proposing that we would then add a new attribute for layer, but have authors manually add the 'only if layers are supported' condition in the media attribute?

tabatkins commented 2 years ago

Yes.

(Notably, this is only going to be required until layer is widely supported.)

mirisuzanne commented 2 years ago

I would be ok with that approach. But I may have been wrong about the media attribute allowing support queries?

domenic commented 2 years ago

So to make sure I'm understanding, the proposal is something like:

<link rel="stylesheet" media="@supports at-rule(@layer)" layer="foo" href="bar.css">

which will fail to parse the value of media="" in old browsers, and thus do nothing there; but it will work in new browsers, and thus be good there. Then, when the transition period is over, could become simply

<link rel="stylesheet" layer="foo" href="bar.css">

That does seem pretty nice. And I think it generalizes to other features in the future, as long as they're @supports()-detectable.

I guess the fact that media="" fails-closed in this way does provide another possibility...

<link rel="stylesheet" media="@layer(foo)" href="bar.css">

if we are comfortable abusing media="" for something that isn't a media query. But I think that could be pretty bad also if you want to actually use media="" for its intended purpose, e.g. media="print" or similar.

tabatkins commented 2 years ago

Following the grammar of @import, it would be media="supports(at-rule(@layer))", but otherwise yeah.

I don't think we should invent a novel microsyntax here.

mirisuzanne commented 2 years ago

This only requires extending media to accept supports() (edit: this would also require a new layer attr), and then encouraging browsers to implement the already-approved at-rule() support function. I assume that extension would happen in the HTML spec?

I agree we don't want a micro-syntax for layers only, but the other similar solution along these lines - maybe even a bit more extensible - would be to say that media accepts 'all @import syntax after the url'. Then it would continue to support any new options added to @import over time? (edit: this would not require a new layer attr, since that would be part of the import syntax)

tabatkins commented 2 years ago

say that media accepts 'all @import syntax after the url'

Strong agree; anything we can do to @import we're going to want to be able to do for <link> as well.

However, I know that media= has some legacy parsing constraints, so I'm curious how feasible that actually is.

lilles commented 2 years ago

Which legacy parsing constraints?

The html spec says <media-query-list> and the Blink implementation does not seem to have any legacy quirks for the link element at least.

mirisuzanne commented 2 years ago

To summarize then, it sounds like the preferred direction would be extending the media attribute to support all @import syntax after the url. For layers, that would look like:

<link rel="stylesheet" media="layer(foo)" href="bar.css">

From an author perspective, I think the ability to share a single syntax between CSS & HTML would be amazing. The only downside I see is the name of the attribute, but that feels workable/teachable. (If there is ever a move to e.g. <style src=''>, then a similar attribute could be supported with a new name.)

I'd be interested in thoughts from @emilio and @smfr.

(Also curious what the next steps are here in terms of WHATWG process?)

domenic commented 2 years ago

(Also curious what the next steps are here in terms of WHATWG process?)

https://whatwg.org/working-mode#changes and in particular https://whatwg.org/working-mode#additions may be helpful.

I'd say the two biggest to-dos are confirming multi-implementer interest (lots of implementers seem involved in the above discussions but they haven't explicitly said "yes we'd implement this particular solution") and writing a spec PR/web platform tests PR. It may be the case that writing the spec PR helps implementers be more confident about what they're agreeing to, so I would personally tackle that first, but on the other hand it could end up wasted effort if they are not interested. So the exact ordering between those is up to the contributors.

yoavweiss commented 2 years ago

Late to the party...

Firefox definitely has a preload scanner of sorts, fwiw, and we do scan for @import rules: https://searchfox.org/mozilla-central/rev/d4b9c457db637fde655592d9e2048939b7ab2854/layout/style/ImportScanner.h

@emilio - thanks! I stand corrected. My opinion stands that for Chromium, it seems unwise to rely on the CSSPreloadScanner implementation for this feature without revamping it. I'll let @smfr speak for WebKit, but from looking at their implementation, it doesn't look significantly sturdier..

All this to say that I'd significantly prefer the proposed markup solutions.

/cc @xiaochengh - for potential opinions on implementing the above.

xiaochengh commented 2 years ago

<link rel="stylesheet" media="@supports(at-rule(@layer))" layer="foo" href="bar.css">

This idea looks the best to me

say that media accepts 'all @import syntax after the url'

This has a con that it makes the media attribute on link elements inconsistent with media on the other elements (meta, source and style). And this means that what we need to get from media is not just a boolean but also a layer name, which is... ugly.

tabatkins commented 2 years ago

say that media accepts 'all @import syntax after the url'

This has a con that it makes the media attribute on link elements inconsistent with media on the other elements (meta, source and style). And this means that what we need to get from media is not just a boolean but also a layer name, which is... ugly.

I'm not sure what you mean by this, Miriam is just saying that we define it in a way that allows for CSS to add more conditional types to @import and have them automatically work, rather than manually copying over the current @import grammar and freezing it until we manually update it again. Maybe you're confusing this with the suggestion from Domenic that we add a media="@layer(foo)" microsyntax?

Which legacy parsing constraints?

The html spec says and the Blink implementation does not seem to have any legacy quirks for the link element at least.

Oh good, I had a half-remembered idea that media="" did some funky parsing stuff around just dropping anything after the first unrecognized part of the query, but now that I think about it more I think there was a legacy parsing thing that resulted in us defining the useless only keyword you can put before the type - it would cause older UAs to ignore the whole thing and treat it as false.

xiaochengh commented 2 years ago

say that media accepts 'all @import syntax after the url'

This has a con that it makes the media attribute on link elements inconsistent with media on the other elements (meta, source and style). And this means that what we need to get from media is not just a boolean but also a layer name, which is... ugly.

I'm not sure what you mean by this, Miriam is just saying that we define it in a way that allows for CSS to add more conditional types to @import and have them automatically work, rather than manually copying over the current @import grammar and freezing it until we manually update it again. Maybe you're confusing this with the suggestion from Domenic that we add a media="@layer(foo)" microsyntax?

I'm talking about the media="layer(foo)" microsyntax. I'm fine with extending it with more CSS conditional types (like @supports, if that's what Miriam means), but I'm not a fan of putting layer in media, because layer is not a conditional.

emilio commented 2 years ago

Yeah, media="layer()" definitely feels off to me.

tabatkins commented 2 years ago

Good, because that was an offhand suggestion by Domenic, and both me and Miriam said we didn't want it. ^_^

lilles commented 2 years ago

Which legacy parsing constraints? The html spec says <media-query-list> and the Blink implementation does not seem to have any legacy quirks for the link element at least.

Oh good, I had a half-remembered idea that media="" did some funky parsing stuff around just dropping anything after the first unrecognized part of the query, but now that I think about it more I think there was a legacy parsing thing that resulted in us defining the useless only keyword you can put before the type - it would cause older UAs to ignore the whole thing and treat it as false.

HTML4 did that. I don't know when it was dropped from the living standard.

mirisuzanne commented 2 years ago

@tabatkins That's not a new microsyntax they're responding to, the layer() keyword and function are part of the @import syntax now. So if we allow @import syntax, that would include a syntax for layers.

tabatkins commented 2 years ago

Oh! Okay, right, so then I agree that should be left out; we just want to carry the conditions over. The layer() syntax isn't part of the import condition, it's an unrelated bit of functionality, and is covered analogously by the layer= attribute.

We can easily rearrange the @import definition to name the condition part in a way that makes it easy to reference from HTML.

mirisuzanne commented 2 years ago

Then the proposed spec changes would be:

The long-term syntax is:

<link rel="stylesheet" layer="foo" href="bar.css">

And the transitional syntax for handling browser support is:

<link rel="stylesheet" layer="foo" media="supports(at-rule(@layer))" href="bar.css">
annevk commented 2 years ago

Should this thread be moved to whatwg/html or will you start a new one there with a summary? Maybe the latter is better at this point?

nachtfunke commented 2 years ago

<link rel="stylesheet" layer="foo" media="supports(at-rule(@layer))" href="bar.css">

So... am I understanding this right that with this new syntax, not only can we declare layers for linked stylesheets, but we can even detect support for them, which we can't do in regular stylesheets at the moment?

bramus commented 2 years ago

@nachtfunke The at-rule(@layer) part is a new addition to CSS that was approved just a few days ago. So you will be able to do it in regular stylesheets as well. I've got a post up on my blog that digs into it.

By having [media] accept all “import conditions”, this new at-rule() function also becomes available to use there.

mirisuzanne commented 2 years ago

Should this thread be moved to whatwg/html or will you start a new one there with a summary? Maybe the latter is better at this point?

@annevk that's my mistake, being new to the WHATWG repos. Happy to move or summarize in the other repo. I can do that this afternoon.

I think at this point we've reached a general consensus on the approach, from people participating? Even though I'm not sure we have official sign-off from implementors, we at least have good input. I'm happy to start on a spec PR (and then platform tests), to get more detailed feedback there.

Thank you all!

annevk commented 2 years ago

@mirisuzanne sounds great. (If you're interested in a general introduction to the WHATWG, https://whatwg.org/working-mode and https://whatwg.org/faq might be useful.)

annevk commented 2 years ago

Let's continue discussion in https://github.com/whatwg/html/issues/7540. Thanks @mirisuzanne!