-
Notifications
You must be signed in to change notification settings - Fork 679
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[css-selectors][css-namespaces] Lexical Scoping #4061
Comments
To summarize the net effect of this proposal, as I understand it: When you import a stylesheet into your project (via 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:
Cons:
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.
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 /* 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 /* 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?
|
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 |
I like @matthew-dean's proposal and I would like to show my support for this thread in general |
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 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 |
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 |
I think the In other words, every HTML component, by default, could have a <div scope="local, document(foo)"></div> Meaning, the local (unscoped) styles apply, as does the global scoped 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. |
Hmm, as far as "guarding" against legacy browsers, I'm not sure the best approach. I'm not sure a type is ideal (and wouldn't be sufficient to "block" use with a legacy browser), but I feel like that's more of a detail. |
@matthew-dean The It would still be complemented by other attributes to define what the scope/namespace should be. The type attribute could be optional, in case an author for some reason wanted the scoped stylesheet to be loaded as a document-level stylesheet in older browsers. |
Oh is it? ^_^ Nice! Learn something new every day. You're right, that could be potentially a much better solution for blocking legacy browsers then a media query hack. Would you still suggest |
Agreed. It'll be a little bit redundant to have both |
@matthew-dean I agree. I think better syntax and behavior around concatenated stylesheets could be arrived at.
I don't see how it's limited. You indicated that this is more flexible: <div scope="all and (foo, bar)"> But my proposal could allow this by declaring that the existing @namespace local(button) {
:scope {
/* styles for the button element */
}
} <button buttons:class> More complex scoping of descendant elements could accomplish this by combinating with
We can certainly add syntax for importing and extending namespaces within CSS.
It doesn't clash with xml namespacing... it is using xml namespacing. That's why I think my proposal is "simple"... it builds on existing primitives already in browsers. I don't see how your proposed syntax avoids namespace collisions, which is a major goal of this proposal. |
I see, so you're anticipating a scenario of "class To be clear, this wasn't originally my proposal, I was just bumping for visibility because of its simplicity. But I think you're pointing out that simplicity could lead to ambiguity. Which may be a fair criticism. There are two pieces I think are awkward in the original proposal:
Instead, what seems more logical to me is for elements to opt into a particular scope (or multiple scopes) or namespace(s). And the nice thing about Rich's scoping proposal is that you don't have to add verbose namespace attributes on every single element all the way down your tree, which I think would be a non-starter for many devs. As far as namespace collision, there would probably be a simple way to target those scopes to a particular selector, such as in-place aliasing, like: <button class="btn" scope="foo(.button as .btn)"> Alternatively, it that proved to be clunky, there could be some kind of aliasing in a style-sheet / style block, where a namespace would be referenced and exported as another namespace (which would probably be cleaner). Either way, that seems more flexible / workable than XML namespacing just because of how people are used to working with CSS. We can't assume / force that people would only work with classes, and not IDs, element names, or attribute selectors when scoping their styles. |
I'm not sure how stylesheet concatenation tools would work in this situation where there are two different namespaced definitions of the same ident but having distinct meanings that shouldn't collide and end up accidentally matching the same elements.
These would have no effect in the browser except to provide hooks for styling. If you wanted the browser button to have a
It doesn't. here's how that would work: @namespace local();
:scope p { color: darkgray; font-size: 16px; } <link rel="namespaced stylesheet" namespace="markdown">
<div id="markdown-container" markdown:class>
<!-- transformed from markdown -->
<p>This text is dark gray and has a font size of 16px.</p>
</div>
I must be doing a bad job at describing it then, because there's actually very few new things that I'm proposing here, it's just using document namespaces and syntaxes that the browser already has, to provide some convenience features to make it easier to leverage those things in the context of a css file.
I agree. I didn't make that assumption. My proposal didn't mention attribute selectors nor tag names because they already work with namespaces in css and in html. |
@chriseppstein Hmm, maybe this syntax is throwing me off. I don't understand this: @namespace local();
:scope p { color: darkgray; font-size: 16px; } <link rel="namespaced stylesheet" namespace="markdown">
<div id="markdown-container" markdown:class>
<!-- transformed from markdown -->
<p>This text is dark gray and has a font size of 16px.</p>
</div> Why does this say |
I mean, I guess what I'm saying is: why attach the namespace to an arbitrary attribute, instead of defining an attribute where one can attach (select) namespaces? I mean, I get that you're trying to use existing syntax, but I guess to me, this syntax is just hard to follow. It also clashes with libraries like Vue (and Ractive?) which use XML-namespace-like attributes to change attribute behaviors. Which of course was dangerous on their part, but... I think also XML namespacing on an attribute isn't supposed to define the tree behaviors, is it? (According to this doc, it explicitly isn't supposed to work that way with attributes?) There just seem to be a lot of semantic and counter-intuitive oddities with this particular syntax direction. It's also notable that web standards bodies moved away from XML semantics, first with XHTML2, which was much more XML-like, and then moving away from requiring explicit namespace declarations for |
@matthew-dean
As a person who has made tools that play around in the domain of standards, that's just par for the course, if the browser changes, you adapt.
Because there's not just one namespace involved. There's usually an application namespace where things can be unique, but each thirdparty library usually has associated css that really aught to be in its own namespace. If you're using something like bootstrap, all those classes should be in their own namespace. I think the idea of using
I'm not sure what this means. I'm not defining any tree behaviors that I know of. I'm just proposing an easy way to define a namespace based on the url of a stylesheet. The rest just falls out from that. |
What I mean is, in the above example, if That is, the way to namespace a "tree" in XML appears to be more like: <!-- Literally would be more like xmlns="http://www.w3.org/1999/xhtml" -->
<link rel="namespaced stylesheet" xmlns="markdown">
<div xmlns="markdown" id="markdown-container">
<p>This text is dark gray and has a font size of 16px.</p>
</div> Which is a bit more syntactically similar to what I was suggesting i.e. setting a namespace explicitly in an attribute (although I would pick something other than So, the only difference I was proposing from traditional XML namespacing was syntax (attribute name): <link rel="namespaced stylesheet" scope="markdown">
<div scope="markdown" id="markdown-container">
<p>This text is dark gray and has a font size of 16px.</p>
</div> And really, otherwise, the CSS usage was pretty similar: @scope foo {
p { }
}
// vs. the more pleasing version you proposed (you had a few different syntax variants?
@namespace local(buttons) {
.button { }
}
@namespace local(links) {
.link { }
} So I think the naming could just as easily look like this, which is closer to your proposal: <!-- or "cssns" to echo "xmlns"? -->
<link rel="namespaced stylesheet" namespace="markdown">
<div namespace="markdown" id="markdown-container">
<p>This text is dark gray and has a font size of 16px.</p>
</div> |
Re: this
That example is a little more clear and makes more sense; however, I would still only expect that to have an effect on that particular element. I think to have a "sub-tree effect", then some kind of namespace identifier is needed. I feel like it's incomplete without it. Meaning: <button app:class="my application classes"> <!-- This applies to this element -->
<p>...</p> <!-- This should receive all <p> global styles and namespace app:p styles -->
</button> ....but.... <button cssns="app" class="my application classes"> <!-- now we're scope-limited -->
<p>...</p> <!-- This should receive all only namespace app:p styles -->
<p cssns="none" bootstrap:class="paragraph">...</p> <!-- turn off inheritance and scope a class -->
</button> What about some kind of blend like that to make it clear when there's a "sub-tree effect", and when I'm specifically applying a namespace'd class? To me, that follows XML namespace semantics a little closer, with respect to the behavior of namespaced attributes. |
Ah, the confusion here is because I wasn't proposing that
That means your expectations match what I was proposing! Huzzah! It looks like you're trying to accomplish scoping selectors to match a subtree of elements like what happens with a web component. That wasn't something that I was proposing. I'm simply proposing a namespacing ability so that we can avoid naming collisions. If the stylesheet had an unscope tagname selector Basically, this works exactly like what you'd expect css-modules to accomplish, it treats class and id identifiers as lexically scoped, but you still have to use combinators with those lexically scoped idents to avoid styles bleeding into other parts of the document. I think the idea of a subtree-scoping capability is interesting. My sense is that web components handle that case well or eventually will be made to do so, so I was hoping to just solve the isolated problem of third-party css files and name collisions which, afaik, still exists as a (theoretical) problem in web components too. |
Isn't what many seem to want just a type of CSS variables containing lists of declarations? Then they could be applied to any elements (or targets identifiable with selectors in general) without touching the markup. (Of course in the common case of components there would be, and often already is, a direct markup based way to select them. I'll assume
Both |
In the above the at-rule's functionality seems to be subsumed with what I've just found in @tabatkins's repo: placeholder selectors (https://github.com/tabatkins/specs/blob/gh-pages/css-extend-rule/index.bs). |
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
.The text was updated successfully, but these errors were encountered: