Open chriseppstein opened 5 years ago
To summarize the net effect of this proposal, as I understand it:
When you import a stylesheet into your project (via <link>
or `@import), you declare a prefix that is implicitly appended to all class or ID selectors in that stylesheet, and maybe other stylesheet identifiers.
This way, you can safely use third-party CSS (or styled components created by different devs within the same large organization) without conflicts, because as the author of the final, composed document, you chooses the prefix and uses it for classes and IDs. You can also override or extend rules from the imported stylesheets by using the prefix in you own selectors.
Pros:
Fairly simple & similar to CSS class-naming conventions that append a component-name prefix to all classes inside the component.
Easy to polyfill by cycling through the rules in the imported stylesheets and modifying their selectors to insert the prefix.
Cons:
As currently proposed, you would get incorrect styles in unsupporting browsers (or as a flash until a polyfill runs) when using the <link>
syntax. The selectors in the imported stylesheet (.some-class
) wouldn't apply to the intended elements (.scope|some-class
), but they could apply to other elements. This could be solved by defining a new rel
type: <link rel="scoped-stylesheet">
or something similar, which would prevent the linked file from behaving as a stylesheet at all unless the scoping proposal is supported (natively or via polyfill). The @import
syntax should already have the same effect, as any unrecognized tokens in that rule should make it invalid.
It only addresses scoping for class and id selectors, not for tag name or attribute selectors. If we lived in a world where XML namespacing was standard for custom tag & attribute names, I'd suggest integrating the two, so the declared prefix also gets linked to the default @namespace
of the imported stylesheet. Maybe that's still a good suggestion! But in a world where custom elements are defined by dasherized names that have no special syntax considerations in CSS, things get more complicated.
It could be very confusing to have two ways of declaring namespace prefixes which affect different selector parts (this proposal for classes & IDs, and XML namespace prefixes for tags and attributes). That said, if CSS is going to have both, with a clear separation to where they apply, reusing the prefix|selector
syntax does make sense.
As I mentioned on Twitter, if this scoping mechanism is going to apply to CSS identifiers, it should be integrated with a scoping mechanism for referencing identifiers from shadow trees, as discussed in #1995.
@AmeliaBR Thanks for writing this up. I think it's a good summary and you've called out some important considerations and back-compat issues.
It could be very confusing to have two ways of declaring namespace prefixes which affect different selector parts (this proposal for classes & IDs, and XML namespace prefixes for tags and attributes).
This is a good point and having two types of namespaces bothers me too.
This proposal so far has taken the perspective that there can be classnames and ids that are defined by the stylesheet and referenced from html. Given that stylesheets tend to be shared across multiple documents, I think this is not an uncommon mental model for developers to use, but it is contrary to the current model that css is just selecting values from html documents.
It occured to me that we could take a slightly different angle. What if a document could easily define an attribute namespace based on a stylesheet's url. In this way we keep the idea that the namespace belongs to the document's attributes. The new semantic then introduced would be that a stylesheet can have its class and id selectors placed either explicitly (from the css file) or implicitly (via import/link) into the namespace of the stylesheet.
Updating my first example, everything remains the same, but the markup for a scoped stylesheet link would become:
<!-- scoped.html -->
<link rel="stylesheet" href="navbar.css" namespace="nav">
<aside>
<ol nav:id="navbar"> <!-- matches, doesn't match #navbar in the global namespace defined by other css files -->
<li nav:class="element">Home</li> <!-- matches, doesn't match other .element in the global namespace defined by other css files -->
</ol>
</aside>
Updating the second example, we can now use @namespace
but a new value of local()
would have the behavior of changing the meaning of the id and class selectors to be selecting the id and class attributes from the namespace of the stylesheet, it can also provide a scope for the identifiers used in css-only constructs like @keyframes
.
/* navbar.css */
@namespace local();
#navbar {
/* main container */
}
.element {
/* stuff contained by the navbar */
}
<!-- unscoped.html -->
<link rel="stylesheet" href="navbar.css">
<aside>
<ol id="navbar"> <!-- matches #navbar in the global namespace so the idents in navbar.css don't match here. -->
<li class="element">Home</li> <!-- matches .element in the global namespace so the idents in navbar.css don't match here. -->
</ol>
</aside>
<!-- scoped.html -->
<link rel="stylesheet" href="navbar.css" namespace="nav">
<aside>
<ol nav:id="navbar"> <!-- matches #navbar in the nav namespace -->
<li nav:class="element">Home</li> <!-- matches .element in the nav namespace -->
</ol>
</aside>
Updating the multi-namespace example we can now use @namespace
as a ruleset parent (previously illegal). These would define a namespace against the url of current stylesheet with a hash identifier added on. That is, @namespace local(foo)
is the same as @namespace foo url(#foo)
but with the extra semantics of lexical identifier scoping.
/* all.css */
@namespace local(buttons) {
.button { }
}
@namespace local(links) {
.link { }
}
@namespace local(call-to-action) {
.cta .buttons|button.links|link {
/* styles for elements having both of these classes */
}
}
<!-- scoped.html -->
<link rel="stylesheet" href="all.css" namespace="#buttons:buttons #links:links #call-to-action:marketing">
<aside marketing:class="cta">
<a href="signup.html" buttons:class="button" links:class="link"> Sign Up! </a> <!-- matches the selector defined in call-to-action.css -->
</aside>
I feel like this provides all the same benefits while at the same time relying on existing browser primitives better.
Do you think it would be possible to extend this scoping to all selectors within a namespace?
@cherscarlett Which selectors do you have in mind?
local|div
could be a div
in the namespace of the current stylesheet.[local|foo="bar"]
or one of the other namespaces assigned via import. I'd be very wary to automatically scope an arbitrary namespace selector. For example, [aria-role="nav"]
has specific meaning in the html namespace.*
selector selects all elements, if it only selected elements with a class in the stylesheet namespace then the better selector to use in that case would be [local|class]
.I feel like this is extremely clunky:
<link rel="stylesheet" href="all.css" namespace="#buttons:buttons #links:links #call-to-action:marketing">
<aside marketing:class="cta">
<a href="signup.html" buttons:class="button" links:class="link"> Sign Up! </a> <!-- matches the selector defined in call-to-action.css -->
</aside>
I mean, not only does the namespace prop get hairy, but the xml-style namespacing is extremely limited, and this form doesn't allow multiple inheritance (or exclusions). It also clashes with xml namespacing, does it not? Regardless, I think something like Rich Harris's suggestion is much more palatable.
e.g.
<style>
@scope foo {
p {
font-family: 'Comic Sans MS';
}
}
</style>
<div css="foo">
<p>Paragraph with scoped styles</p>
</div>
The reason why this is hugely powerful is because you could extend it in a media-query-like fashion, which namespacing doesn't provide any flexibility for. I might also change the HTML attribute. As in:
<style>
@scope foo {
p {
font-family: 'Comic Sans MS';
}
}
@scope bar {
p {
color: red;
}
}
</style>
<div scope="only (foo, bar)">
<!-- or this would just be the default if scope is defined -->
<p>Paragraph with foo and bar styles only</p>
</div>
<div scope="all and (foo, bar)">
<!-- Allows opt in to global styles plus specific scope styles -->
<p>Paragraph including global p and foo and bar styles</p>
</div>
Just like media queries being either an at rule or a HTML prop, you should also be able to wrap a stylesheet in a scope to make it not apply globally. As in:
<link rel="stylesheet" href="all.css" scope="foo">
Scoped styles could be inheritable in Web Components (since they are opt in by default, and do not apply globally), which would make it orders of magnitude easier to implement / manage than the proposed adoptedStyleSheets
JS interface (not that that shouldn't happen, it's just an unruly interface for something that can be relatively simple declaratively).
I like @matthew-dean's proposal and I would like to show my support for this thread in general
Just like media queries being either an at rule or a HTML prop, you should also be able to wrap a stylesheet in a scope to make it not apply globally. As in:
<link rel="stylesheet" href="all.css" scope="foo">
this will load all.css in all current and legacy browsers.
if we want certain css to load conditionally we should reuse either rel
, media
or type
. the latter two don't make much sense in case of scoped css, so we might need a new linktype for scoped css...
this will load all.css in all current and legacy browsers.
That's a good point.
You could always prevent a media match with:
<link rel="stylesheet" href="all.css" media="scope" scope="foo">
AFAIK, any non-matching media-query is supposed to be converted to not all
according to the spec
I think by the same token, you could extend those queries in scope-supporting engines like:
<link rel="stylesheet" href="all.css" media="(min-width: 600px) and scope" scope="foo">
... which would be the equivalent of:
@scope foo {
@media (min-width: 600px) {
// all of all.css
}
}
(Note that the word scope
in the <link>
media query has no special meaning; it's simply an unrecognizable query to a legacy browser which would make it ignore application of the stylesheet.)
Note that the word scope in the media query has no special meaning;
I'm not sure that's the best way to describe it. Better to define it as an official media query that evaluates as a Boolean representing whether the user agent supports scoped syntax. Which is a bit of a stretch of how media queries generally work (a user agent that recognized the query would presumably never evaluate to false), but is reasonable considering the benefits.
Could we use type="scoped"
instead? (Similar to <script type="module">
.)
I think the local
keyword mentioned earlier is fairly useful, and I would extend that in this syntax to be the essentially the boundary root i.e. normally the document or the shadow root if within there.
In other words, every HTML component, by default, could have a scope="local"
value. But in a shadow root, I could then do this:
<div scope="local, document(foo)"></div>
Meaning, the local (unscoped) styles apply, as does the global scoped foo
(since this is an "or" join, so either query can match). A vendor applying styles would just need to know which scoped stylesheets / stylesheet blocks to apply for the cascade.
There may be a little syntactic massaging needed there, but a query-style syntax seems less verbose and yet more powerful than the namespaced-style syntax.
@valtlai
Could we use type="scoped" instead? (Similar to Githubissues.
Githubissues is a development platform for aggregating issues.
Relevant Specs:
A common complaint about CSS is that everything is global.
I know of at least a couple community proposals that attempt to work around this issue, but they seem to introduce javascript as an intermediary.
I understand that web components have their own notion of a scope local to the component. But this doesn't address the notion of global scope, it just reduces the document surface and the number of selectors in the global scope. There's still no mechanism to address a naming collision.
So why not allow stylesheets and web pages to opt into lexical scoping for name references?
Example of global css consumed into a namespace:
Example of lexically scoped, namespaced css
Example of css involving multiple namespaces
Example of multiple namespaces in a single file (concatenation)
Open Questions:
@keyframes
and grids? (Probably yes)@import
, it seems like we'd need to allow nested scope references. E.g..scope-1|scope-2|some-class
as well as nested@ident-namespace { }
@namespace
directive? (Probably not)@import
.