Closed mirisuzanne closed 3 years ago
It strikes me that in some ways specificity already works in isolated layers:
Clearly that was the original intent: components (IDs) override patterns (classes/attributes) override defaults (elements). But it maybe falls apart in modern use for a few reasons:
That could lead us down a path of providing more specificity layers rather than origin layers? I'm not sure…
The selector-specificity
:where()
approach is both narrowly-applied and removes all specificity, which could still be a useful tool within origins
Its goal is exactly the opposite: because it's a pseudo-class, it allows only removing part of the selector from the specificity calculation, enabling authors to distinguish importance-signifying criteria from merely filtering criteria. Of course it can be used for the whole selector, but it doesn't have to be, nor was that the primary use case I had in mind when I proposed it.
The CSS Working Group just discussed Custom Origins
.
came here via one of Jen's Tweet. https://twitter.com/jensimmons/status/1219351448028356609
* IDs are explicitly one-off, so we can't re-use anything in the component layer
well, actually CSS doesn't bother. You can have multiple elements with the same ID and they'd be styled according to the current rules. It's JS that'd likely choke or sth. like a XSLT processor.
Maybe I got lost in translation reading this proposal (and what happend around Jen's tweet) and I'm going to state the obvious; but here are my 2ct.
FWIW both Cascade and Specificity are and always have been perfectly fine the way they are: the closer to the element (origin/location/cascase) considering the selector specificity. Done. What's not to grasp? :) It's kinda like with using Layers and Masks in Photoshop: some ppl just can't get their head around it :) If you one doesn't understand pointers in C++ or references iin Java[Script] s/he equally screwed.
However, the culprits IMHO happen to be "modern" design patterns and philosophies like BEM or Atomic Design & such that are causing the issues ppl. apparently have with "specificity": you either have a rather flat hierachiy where everything has virtually the same specificity (i.e. BEM) (and origin/order matters) or the HTML is poluted with a gazillion cryptic utility classes and what-not that might end up raising the specificity over the roof. And then there are some Framework / Library creators that don't get it either and butcher our beautiful Cascade adding !important
all over the place.
There's only one addition to the Level in the Origin line of things I'd imagine to be helpful in some situations and to some people. It'd be considerably 1. logical and 2. following the current rules of the Cascade but applied a different "weight" to stylesheets served from 3rd party domains (CDN et al) and one's own (sub-)domain. That's something crafty people can easily understand and hopefully handle responsibly. That's also how CDN based CSS Frameworks would get a lower significance than author and user styles. Currently only source order matters, IIRC.
With such a distinction equal selectors and equally specific selectors in an author style (even if @import
ed) served from the same domain/origin as the document could overrule one from a CDN'ed Framework w/o using !important.
Here's how things could work bold = "new" origin level (out of my head from lower to higher priority)
link
with @import
3rd party domain (external origin)link
3rd party domain (external origin)link
with @import
to author styles (same/local origin)link
to author styles (same/local origin)<style>@import
<style>
style
attribute / JS!important
.Since this additional level of origin would change the current browser behaviour, authors should "opt-in" to enable the new origin level by adding a <meta>
tag to the document or sending a response Header
.
That's similar to the well accepted <meta viewport>
which also changes the way CSS works with dimensions, or how oldIE was told to toggle its X-UA-Compatible
mode back in the days.
René
I am very excited by this proposal, and have definitely wished for this exact thing. Even if authors can't define n origins, just two would go a long way: One for "base" styles, and another for "module" styles.
I think in most modern development, there are fundamental differences between these two types of styling. Ideally, I would typically want base styles to apply to the whole page, even piercing into Shadow DOM; but they would be of a lower priority origin, so module styles would always override them. I'd even be interested in a way to do something like all: unset
that reverts my module styles but leaves my base styles applied. Related comment: https://github.com/w3c/csswg-drafts/issues/3547#issuecomment-458232087
applied a different "weight" to stylesheets served from 3rd party domains (CDN et al) and one's own (sub-)domain.
I think this might be useful to some, but it only addresses one specific problem that's a subset of this problem as a whole. Not everyone pulls in (say) Bootstrap from a CDN and then sticks some overrides on it. How would this help me override my own base styles? For me, I need more control over my styles and how they are applied.
Having N author-defined origins is realistically no more difficult than having two (in other words, the spec text and probably implementation for both would be nearly identical, just with one being locked into 2), so that's nice at least.
Not everyone pulls in (say) Bootstrap from a CDN and then sticks some overrides on it.
by CDN I simply mean "not the same hostname".
I often set up one or more subdomains on the very same server pointing to the same files to overcome the connection limits (esp. for HTTP/1.1). So cdn.myserver.tld
or img.myserver.tld
and myserver.tld
are actually the same machine and share the same root. Some trickery in Apache's .htaccess
makes sure that only certain file types are delivered by and only accessible via the CDN URLs.
There you have you own "CDN" to stick some overrides on it :) Same simple rules apply.
I think this proposal is very much needed from the point of view of components builders. But I'd also like to stress that a shared component would often involve two types of styles:
display: flex
for some horizontal layout). Maybe not quite !important
, but the desire is to make it very hard to change this style from author/user stylesheets.:focus {outline: ...}
, etc.Someone above mentioned similarity with z-index
and that seems to fit pretty well conceptually.
Another point I wanted to make is that ideally whatever spec ends up here, it'd be polyfillable with the today's technology. Perhaps a WebPack plugin could be provided that would order style/link
tags or selectors to respect the new specificity ordering. This could limit the spec quite a bit, but backward compatibility is critical for this, imho.
I'm working to break out some more specific topics for consideration, but I think this could be a prime candidate for more face-to-face discussion.
I made a codepen demo that demonstrates how I currently approximate both "origins" and "scope" using custom properties:
In that way, each origin (variable) in the stack allows for scope (inheritance) internally, but higher origins will always take precedence.
We've written up a draft syntax proposal that addresses most of the open issues here:
https://gist.github.com/mirisuzanne/4224caca74a0d4be33a2b565df34b9e7
Marking for review with the full group…
Here's my feedback to this proposal.
On the more positive side, this could give newcomers a clearer placer where to “put their stuff”.
Currently libraries like Bootstrap in their SCSS variant already have ordered imports that more or less follow a logical specificity graph (from base to class level to utilities).
You can already choose to simply extend that system by adding a component between those imports. But it does take some expertise to know exactly how to split things up.
Over the years I've employed numerous strategies to work with the cascading aspect of CSS in a good way, in both the context of a non-namespaced framework (Bootstrap) or in specific contexts that require high degrees of CSS knowledge to execute well (BEM/ITCSS).
I find that newcomers have a hard time looking "through the framework" knowing where they can divide it into pieces and start extending it.
Let's say you have a Bootstrap-like system that is established that is set up like this:
@layer reset, objects, components, utilities;
Then the newcomer could be instructed to write their component in the components layer:
@layer components {
.my-component {
/* some style rule */
}
}
When the CSS gets bigger, organising could be done as such:
@layer components {
@import url(button.css);
@import url(panel.css);
@import url(tabs.css);
}
(or rather with a preprocessor so that we don't have too much HTTP requests)
With this proposal I can see some novel logic happening in the theming world. Imagine you have to add theming to the above code.
@layer reset, objects, components, theme, utilities
@layer components {
@import url(button.css);
@import url(panel.css);
@import url(tabs.css);
}
@layer theme {
@import url(button-theme.css);
@import url(panel-theme.css);
@import url(tabs-theme.css);
}
Your theme can now live on a clear layer where it will always override the components layer.
I see a lot of people who don't really get the cascade. One risk of this is that it will simply be used an an organizational method while it does have specificity side effects, creating new problems.
Imagine this:
@layer reset, objects, framework-components, custom-components, utilities
In this example the custom components will have a higher specificity than the framework components. The project team is using the layers for organisation purposes:
@layer framework-components {
@import url(button.css);
@import url(panel.css);
@import url(tabs.css);
}
@layer custom-components {
@import url(reverse-tabs.css);
@import url(logo.css);
}
But there's actually no reason they should. They should have a similar level of specificity. It would probably be better to write:
@layer reset, objects, components, utilities
@layer components {
@import url(framework/button.css);
@import url(framework/panel.css);
@import url(framework/tabs.css);
@import url(custom/reverse-tabs.css);
@import url(custom/logo.css);
}
I welcome the efforts to improve upon CSS. I really don't like the flat non-cascading solutions that people escape to nowadays (like Tailwind CSS). I think the cascade can be such a strong tool if used correctly. If we can put tools in people's hands to use it in smarter ways that would be cool. But I think getting people to use this might be a challenge in itself.
I think this is a well-written proposal. Thank you for such a clear exposition. I think it definitely helps us explore the space of how far we could go with layer semantics. I also left a few questions and thoughts in the comment thread on the gist.
My summary feedback:
I think the block syntax looks good.
I have some concerns about the external-sheet loading syntax, in particular: whether developers will find it too confusing or difficult to debug, implementation complexity, and the potential for race conditions (example).
I have a concern about how well this proposal satisfies the use case of importing common third-party styling libraries.
This proposal does seem to be backward-compatible with an incremental implementation and shipping approach, along the lines of what I suggested here, which is great. (We'd pre-define certain built-in layers, not allow defining new ones, and not support url import syntax.)
It's also good that it appears this approach is also polyfillable.
the potential for race conditions (example).
As stated over in a comment on the gist, "order" means standard stylesheet ordering, the same ordering used for the final specificity tiebreaker and many other features (such as "last-defined" for @keyframes with the same name). So there's no race conditions here. If an early stylesheet loads late, it'll just insert its defined layers into the appropriate spot in the ordering, not append to the end.
whether developers will find it too confusing or difficult to debug,
This example is arguing against being able to import a sheet into a layer. Is this because of the temporal race condition confusion? Is it resolved now that it's clear there aren't race conditions?
I have a concern about how well this proposal satisfies the use case of importing common third-party styling libraries.
I'm pretty sure this is also caused by confusion over the race condition thing, and thus isn't actually an issue. Is this right?
This proposal does seem to be backward-compatible with an incremental implementation and shipping approach, along the lines of what I suggested here, which is great. (We'd pre-define certain built-in layers, not allow defining new ones, and not support url import syntax.)
I don't think this is easily compatible with predefined layers. I mean, we could do it, but it would be weird. We'd have to define a specific position for the predefined layer, and either add some more complex syntax to let custom layers go above or below it, or just put all custom layers in a specific position relative to it. I don't want to do the first, and the second means, in practice, that people just shouldn't use the predefined layer at all once custom layers are supported (since it's unlikely that the predefined name, if it fits in their name system at all, is appropriate to be at the start/end of their other layers).
As stated over in a comment on the gist, "order" means standard stylesheet ordering, the same ordering used for the final specificity tiebreaker and many other features (such as "last-defined" for @Keyframes with the same name). So there's no race conditions here. If an early stylesheet loads late, it'll just insert its defined layers into the appropriate spot in the ordering, not append to the end.
I think you're referring to this comment. Agree that it clears my concern about race conditions. There is though the (lesser, but real) concern about flipping ordering during load causing style thrashing.
I have a concern about how well this proposal satisfies the use case of importing common third-party styling libraries.
I'm pretty sure this is also caused by confusion over the race condition thing, and thus isn't actually an issue. Is this right?
My concern was regarding this comment I made in the gist: about how to avoid the main site knowing about the third-party layer names (this comment). @lilles mentioned a related concern.
As @tabatkins mentioned to me offline, I think my main concern is alleviated because the main stylesheet can wrap any third-party imported stylesheet in an anonymous or named layer, and then make sure to declare that layer at the right place relative to other main stylesheet layers.
I don't think this is easily compatible with predefined layers. I mean, we could do it, but it would be weird. We'd have to define a specific position for the predefined layer, and either add some more complex syntax to let custom layers go above or below it, or just put all custom layers in a specific position relative to it. I don't want to do the first, and the second means, in practice, that people just shouldn't use the predefined layer at all once custom layers are supported (since it's unlikely that the predefined name, if it fits in their name system at all, is appropriate to be at the start/end of their other layers).
If we first ship one or more predefined layers, and then later ship custom-definable layers, then yes the ordering relative to the predefined layers would probably be fixed. However, this would just mean the site should at that point stop using the predefined layers and use custom layers only, if it conflicted with their desired order, and then the ordering of predefined layers relative to custom layers doesn't matter, because the site doesn't use them. If the site included a third-party stylesheet that utilized the predefined layers, then it could be imported with a wrapping anonymous or named layer placed at the appropriate ordering position; this situation seems the same as the one in my earlier comment above.
Therefore I do think it's backwards compatible. If predefined layers are specified and shipped first, this has the following advantages as I see it:
@import
, link
and style tags
, or the nested layer concatenation and ordering features If we first ship one or more predefined layers, and then later ship custom-definable layers, then yes the ordering relative to the predefined layers would probably be fixed. However, this would just mean the site should at that point stop using the predefined layers and use custom layers only, if it conflicted with their desired order, and then the ordering of predefined layers relative to custom layers doesn't matter, because the site doesn't use them.
Right, so at that point we're defining something that we know will be obsoleted almost immediately, but will provide a footgun (in the form of a name that authors aren't allowed to use) forever. Do we believe authors are clamoring for a solution that can be well-solved by a single predefined layer so hard that we think it's worthwhile to throw away effort like that?
Avoids all of the potential developer gotchas referenced on the gist having to do with nested layers
I just reread the gist and couldn't find any gotchas listed about nested layers. Can you elaborate?
Reduced complexity of understanding the feature for developers
Yes, a single predefined layer is simpler than multiple layers. But we're gonna move to arbitrary named layers anyway; they can't avoid that complexity. And having both a predefined layer and arbitrary layers makes the entire feature slightly more complex than it would otherwise be, so in the end it's slightly worse, not neutral.
(maybe?) Encourages shared CSS design patterns involving layers among different libraries, thereby making it easier for sites to adopt these libraries without learning new layer names and best practices.
I think this one's a stretch, yeah. Even with only one layer, there's still at least two completely distinct and very reasonable ways to use them that would cause bad clashes between libraries: with the layer as reset/defaults and unlayered code as normal, or with the layer as "normal" and the unlayered code as spot-overrides ("better !important").
Right, so at that point we're defining something that we know will be obsoleted almost immediately, but will provide a footgun (in the form of a name that authors aren't allowed to use) forever. Do we believe authors are clamoring for a solution that can be well-solved by a single predefined layer so hard that we think it's worthwhile to throw away effort like that?
If the second part were definitely going to ship soon after, then I would agree with you. I am suggesting we ship the first part and see if the additional complexity is warranted.
I just reread the gist and couldn't find any gotchas listed about nested layers. Can you elaborate?
Specificity/layer depends on method of importing (here). Possible confusion about how order of layer ordering works across stylesheets. Multiple loading. Need to remember to put the layer on all APIs that reference a stylesheet.
Yes, a single predefined layer is simpler than multiple layers. But we're gonna move to arbitrary named layers anyway; they can't avoid that complexity. And having both a predefined layer and arbitrary layers makes the entire feature slightly more complex than it would otherwise be, so in the end it's slightly worse, not neutral.
I don't think it's clear to me that we will need arbitrary named layers. I do think there is likely a good argument for more than one predefined layer though. I wasn't saying it's either one layer or arbitrary layers, there is a midpoint.
I think this one's a stretch, yeah. Even with only one layer, there's still at least two completely distinct and very reasonable ways to use them that would cause bad clashes between libraries: with the layer as reset/defaults and unlayered code as normal, or with the layer as "normal" and the unlayered code as spot-overrides ("better !important").
I'm not sure what the difference is between these two interpretations, can you clarify?
AIUI the current gist proposes that defined layers have higher specificity than un-layered style rules. This would mean that if a third-party library wanted to use a layer that was earlier in the cascade than an existing site (e.g. to provide a base layer for its components that are meant to be overridable by the site), then it's not possible to do this without changes to the site's style sheet to add a layer for it.
This will make it harder to deploy updated third-party libraries without waiting for sites to adopt custom layers first.
I don't think it's clear to me that we will need arbitrary named layers.
To me, this is fundamental. The ability for authors to name & define their own layering was the entire goal of my proposal. There are enough different use-cases being discussed I would expect predefined layers to backfire. Authors will use them differently on different projects, hacking them to solve problems they weren't "approved" for. The same way we hack specificity and importance to achieve these things today. Without the ability to create arbitrary layers, there is no way to "encapsulate" anything, and we're back to a global-namespace hack-layering war that this proposal is meant to address.
Assuming everyone will be fine sharing three globally predefined (specificity) layers is exactly the current problem.
Specificity/layer depends on method of importing
Nesting doesn't change the specificity of a layer. Nesting is only a way to name and group layers, nothing more. Adding multiple anonymous layers via import has no actual impact on the resulting weight of a style.
AIUI the current gist proposes that defined layers have higher specificity than un-layered style rules.
Check again. Unlayered are the highest "normal" layer, and the lowest !important
layer. Explicit layers would only override existing styles when !important
is applied. This could still potentially cause an issue for someone upgrading 3rd-party tools without checking? But that's already a danger with specificity and importance. The danger here is not new, we're only offering a possible new solution to this existing problem.
See also: authors accidentally forgetting a helpful line of code, or triggering layout jank via lazy-load.
Inter-file specificity conflicts already exist. They already take source-order into account as a meaningful cascade metric. Lazy-loading CSS can already trigger jumpy rendering, purely based on selector specificity and the source-order of imports. But the only solution right now is to manipulate & delicately balance selectors. That's a hack, and it makes the problem even more fragile. It breaks the semantics of selector-specificity, and the resulting code does not in any way convey the layering intended.
It would be great if authors had a way to solve that problem with a small change to their import declaration, or a single line of code that makes the layering explicit. That's what we're trying to do. We can't make every possible cascade issue impossible. We can provide more flexible, semantic, and robust tools for authors to address these issues when they come up.
Check again. Unlayered are the highest "normal" layer, and the lowest
!important
layer.
Ok great. I misread the gist then.
Nesting doesn't change the specificity of a layer. Nesting is only a way to name and group layers, nothing more. Adding multiple anonymous layers via import has no actual impact on the resulting weight of a style.
Yes, but the name of the layer depends on its import method, right? Therefore it determines the order of application of style rules due to the ordering of the layers.
Inter-file specificity conflicts already exist. They already take source-order into account as a meaningful cascade metric. Lazy-loading CSS can already trigger jumpy rendering, purely based on selector specificity and the source-order of imports. But the only solution right now is to manipulate & delicately balance selectors. That's a hack, and it makes the problem even more fragile. It breaks the semantics of selector-specificity, and the resulting code does not in any way convey the layering intended.
I agree that versions of this problem already exist.
the name of the layer depends on its import method, right? Therefore it determines the order of application of style rules due to the ordering of the layers.
The method of import can be used to add (and optionally name) a grouping layer around the file contents, if that's what you mean.
It sounds like you see that as a downside? I see that as a way to give the entrypoint file final control over how all layers will be used, which will help authors avoid global naming conflicts. This way a third-party tool is able to hide or expose whatever layers they want, and the consuming document can decide to either interact with those individual layers, or encapsulate them inside a namespace to avoid conflicts.
In either case, with or without an layer-import syntax, the importing document would be able to re-order any exposed layers inside the imported file. By providing an explicit syntax, authors have more control over how that interaction should or should not happen.
Yes, it's very important that the final page author have the ultimate control over how things are layered. A third-party library gives its layers a default ordering according to the order they appear in that library, but the page author can choose to reorder those (or rather, insert their own layers between the third-party's layers) if they have a need for that.
The importing syntax used is just about whether you wrap the entire third-party sheet in another layer or not; doing so lets you (a) put CSS that's not layer-aware into a layer, so it works better with the rest of the layer-aware page, and (b) easily put a whole stylesheet into a particular spot in the layer ordering, without having to carefully control the location it shows up in the document or care about what layer names the stylesheet might itself use.
The method of import can be used to add (and optionally name) a grouping layer around the file contents, if that's what you mean.
It sounds like you see that as a downside?
Not necessarily, I'm just saying this may be a source of developer confusion because of examples like the one I gave in the gist. (I'm not sure this is a big problem, I'm just going through all of the possible concerns and corner cases of this proposal to understand it better and discuss.)
The CSS Working Group just discussed [css-cascade] Custom Cascade Layers (formerly "custom origins")
, and agreed to the following:
RESOLVED: move this proposal into the spec
Initial editor's draft: https://drafts.csswg.org/css-cascade-5/ (sections 6.1 and 6.4)
Closed by CSSWG Resolution to publish FPWD.
Uncovered some earlier notes about this topic, after @mirisuzanne had introduced and got enthusiasm about the topic, but before we had consensus on what the syntax ought to be. I don't think they're particularly relevant at this point, but just in case someone's looking for details on history and early stages, here they are: https://lists.w3.org/Archives/Public/www-archive/2021Jul/0007.html
This relates to the Cascade Specification, along with a number of "specificity" concerns and proposals (such as #2272 & #3890 & the
:where()
selector).Much of my work with design systems has revolved around helping companies define layers of abstraction: building tokens, then defaults, then patterns, components etc. That's a common approach, whether we call it OOCSS or Atomic Design or ITCSS or something else. In order to do that, we often have to be very careful with matching specificity to layer – so components override patterns, and so on – and third-party tools can easily break a delicate balance.
It strikes me that cascading origins &
!important
are designed to solve that same problem on a larger scale (UA, user, author), and then reverse-order for!important
styles. It's a pretty clever solution, but!important
is a blunt instrument for handling layers inside the author origin.I doubt most developers think about cascading origins, or the role importance plays in it - and at this point they don't really need to for practical reasons. I don't have a full solution here, but a rough sense that providing control of custom cascade origins (within/around the author origin) might help:
!important
are designed, and how they work under the hoodA few notes on finding a syntax/approach that would work:
!important
approach (or!default
proposal which I like) is useful in other situations, but too narrowly applied for this particular use-case:where()
approach is both narrowly-applied and removes all specificity, which could still be a useful tool within origins@import
or an at-rule of some kind (e.g.@origin
?). That feels like the more proper scope for this problem.I realize this gets difficult to define quickly:
@origin
blocks internally, and I want to load it in a specific layer within my overall code. Are there layering contexts, similar toz-index
? Nesting origins would get complicated quickly.I hope that all makes some sense. I'd be curious for additional thoughts on this, and happy to clarify anywhere I can.