Skip to content
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

Open
chriseppstein opened this issue Jun 26, 2019 · 26 comments
Open

[css-selectors][css-namespaces] Lexical Scoping #4061

chriseppstein opened this issue Jun 26, 2019 · 26 comments

Comments

@chriseppstein
Copy link

chriseppstein commented Jun 26, 2019

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:

/* navbar.css */
#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 -->
      <li class="element">Home</li>  <!-- matches .element in the global namespace -->
  </ol>
</aside>
<!-- scoped.html -->
<link rel="stylesheet" href="navbar.css" ident-namespace="nav">
<aside>
  <!-- using the html namespace sigil here... is that a bad idea? -->
   <ol id="nav:navbar">  <!-- matches, doesn't match #navbar in the global namespace defined by other css files -->
      <li class="nav:element">Home</li>  <!-- matches, doesn't match other .element in the global namespace defined by other css files -->
  </ol>
</aside>

Example of lexically scoped, namespaced css

/* navbar.css */
@ident-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" ident-namespace="nav">
<aside>
   <ol id="nav:navbar">  <!-- matches #navbar in the nav namespace -->
      <li class="nav:element">Home</li>  <!-- matches .element in the nav namespace -->
  </ol>
</aside>

Example of css involving multiple namespaces

/* buttons.css */
@ident-namespace local;
.button { }
/* links.css */
@ident-namespace local;
.link {}
/* call-to-action.css */
@import url(button.css) as buttons; <!-- should this namespace get exported? -->
@import url(link.css) as links; <!-- should this namespace get exported? -->
@ident-namespace local;
/* using the css namespace sigil here... is that a bad idea? */
.cta .buttons|button.links|link {
  /* styles for elements having both of these classes */
}
<!-- scoped.html -->
<link rel="stylesheet" href="buttons.css" ident-namespace="buttons">
<link rel="stylesheet" href="links.css" ident-namespace="links">
<link rel="stylesheet" href="call-to-action.css" ident-namespace="marketing">
<aside class="marketing:cta">
   <a href="signup.html" class="buttons:button links:link"> Sign Up! </a> <!-- matches the selector defined in call-to-action.css -->
</aside>

Example of multiple namespaces in a single file (concatenation)

/* all.css */
@ident-namespace buttons {
  .button { }
}

@ident-namespace links  {
  .link { }
}

@ident-namespace call-to-action {
  .cta .buttons|button.links|link {
    /* styles for elements having both of these classes */
  }
}
<!-- scoped.html -->
<link rel="stylesheet" href="all.css" ident-namespace="buttons:buttons links:links call-to-action:marketing">
<aside class="marketing:cta">
   <a href="signup.html" class="buttons:button links:link"> Sign Up! </a> <!-- matches the selector defined in call-to-action.css -->
</aside>

Open Questions:

  • Should this be applied to other idents like @keyframes and grids? (Probably yes)
  • Can there be a way to specify a reference to an ident namespace for a url without fetching the resource there?
  • If we allow exporting namespaces via @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 { }
  • Is there any reason to try to merge this notion of an identifier namespace with the xml @namespace directive? (Probably not)
  • How to consume multiple namespaces via a single @import.
@AmeliaBR
Copy link
Contributor

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.

@chriseppstein
Copy link
Author

chriseppstein commented Jun 27, 2019

@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.

@chriseppstein chriseppstein changed the title [css-selectors] Lexical Scoping [css-selectors][css-namespaces] Lexical Scoping Jun 27, 2019
@cherscarlett
Copy link

Do you think it would be possible to extend this scoping to all selectors within a namespace?

@chriseppstein
Copy link
Author

chriseppstein commented Aug 13, 2019

@cherscarlett Which selectors do you have in mind?

  • I think tagname selectors should continue to be in the html default namespace. If there's a desire to select a tagname in the local namespace there's already a syntax for that: E.g. local|div could be a div in the namespace of the current stylesheet.
  • For generic attribute selectors, there's already a syntax to refer to an attribute in a namespace so those can use [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.
  • The * 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].
  • pseudo-element and psuedo-class selectors don't have a relevant namespace.

@matthew-dean
Copy link

matthew-dean commented Aug 15, 2019

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).

@FremyCompany
Copy link
Contributor

I like @matthew-dean's proposal and I would like to show my support for this thread in general

@myakura
Copy link
Contributor

myakura commented Aug 16, 2019

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...

@matthew-dean
Copy link

matthew-dean commented Aug 16, 2019

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.)

@AmeliaBR
Copy link
Contributor

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.

@valtlai
Copy link
Contributor

valtlai commented Aug 17, 2019

Could we use type="scoped" instead? (Similar to <script type="module">.)

@matthew-dean
Copy link

matthew-dean commented Aug 19, 2019

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.

@matthew-dean
Copy link

@valtlai

Could we use type="scoped" instead? (Similar to <script type="module">.)

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. type="scoped" doesn't really specify what the scope identifier should be, so it would be requiring two new attributes instead of one, since specifying the scope on a link would implicitly indicate that it's a scoped stylesheet (a "type" of "scoped").

@AmeliaBR
Copy link
Contributor

@matthew-dean The type attribute for stylesheets already exists. Just like with scripts, unrecognized values prevent the styles from being applied (demo). So it is actually a very good tool to use for this purpose (preventing a scoped stylesheet to be loaded by legacy browsers that don't recognize the new scoping mechanism, without needing to edit the stylesheet itself to add an @scope{} rule).

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.

@matthew-dean
Copy link

@AmeliaBR

@matthew-dean The type attribute for stylesheets already exists. Just like with scripts, unrecognized values prevent the styles from being applied

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 type to specify a scope name though? It would still make sense to me to make it a separate attribute.

@AmeliaBR
Copy link
Contributor

Would you still suggest type to specify a scope name though? It would still make sense to me to make it a separate attribute.

Agreed.

It'll be a little bit redundant to have both type="scoped" and scope="scope-name" attributes, but I think it's worth it to keep them logically separate. And as I said, we could make the type optional if there's also a scope attribute, so it would only be needed as a transitional supports test, and could eventually be dropped once legacy browsers are no longer a concern.

@chriseppstein
Copy link
Author

I mean, not only does the namespace prop get hairy

@matthew-dean I agree. I think better syntax and behavior around concatenated stylesheets could be arrived at.

but the xml-style namespacing is extremely limited

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 :scope selector matches the namespaced class attribute with an empty/non-present value of "". For example:

@namespace local(button) {
  :scope { 
    /* styles for the button element */
  }
}
<button buttons:class>

More complex scoping of descendant elements could accomplish this by combinating with :scope.

and this form doesn't allow multiple inheritance (or exclusions).

We can certainly add syntax for importing and extending namespaces within CSS.

It also clashes with xml namespacing, does it not?

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.

@matthew-dean
Copy link

matthew-dean commented Aug 26, 2019

@chriseppstein

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 .foo from local.css but NOT class .foo from global.css"

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:

  1. namespace="#buttons:buttons #links:links #call-to-action:marketing" <-- I think that should be inherently disallowed. That is, if a stylesheet opts into namespacing as a <link> attribute, it should do so under a single namespace. If a stylesheet has multiple namespaces, they should be within the stylesheet. This would follow semantically with media queries, where one query can be on a <link>, or a stylesheet can contain multiple queries (or both).
  2. The repetition of attributes like buttons:class="foo" links:class="foo" call-to-action:class="foo" I get is utilizing XML namespacing, but I feel this is harder to reason about from the perspective of CSS selectors. That is, CSS can select an ID / class / element / attribute, and of course, this doesn't work to do buttons:id="foo" links:id="foo" call-to-action:id="foo" and then you get into weird cases where the browser doesn't know what to do with buttons:type="text" links:type="text" call-to-action:type="submit". So, in other words, this pattern breaks if your CSS contains anything other than classes, such as trying to style all <p> tags within a local tree. Your proposal seems to clash with the fundamentals of CSS (and possibly the DOM?).

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.

@chriseppstein
Copy link
Author

chriseppstein commented Aug 26, 2019

That is, if a stylesheet opts into namespacing as a attribute, it should do so under a single namespace.

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.

then you get into weird cases where the browser doesn't know what to do with buttons:type="text" links:type="text" call-to-action:type="submit".

These would have no effect in the browser except to provide hooks for styling. If you wanted the browser button to have a type=submit you'd have to have that attribute in the html namespace. The stylesheets could select the type attribute in the html namespace with [type=submit] and a stylesheet could select the the links:type attribute with the namespaced attribute selector: [links|type="text"]. A stylesheet could even select elements at the intersection of these namespaces with something like [type="submit][call-to-action|type="submit"].

So, in other words, this pattern breaks if your CSS contains anything other than classes, such as trying to style all <p> tags within a local tree.

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>

Your proposal seems to clash with the fundamentals of CSS (and possibly the DOM?).

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.

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 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.

@matthew-dean
Copy link

@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 markdown:class when it has nothing to do with a class name? Are you just using XML namespaces to attach a namespace to the element? If so, why on the class attribute? It just feels arbitrary and odd. A class is just a class (or list of them). I don't see why it has anything to do with selectors based on what you wrote in your CSS. If I'm just selecting <p>, why do I need :class ? 🤔

@matthew-dean
Copy link

matthew-dean commented Aug 26, 2019

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 <svg>. In both cases, the syntax moved towards expanding native attributes / element names so that <foo:bar xlink:href> wouldn't be necessary. So I fear that using XML-like namespacing would ultimately be a dead end, which would be a shame, since CSS needs something like this.

@chriseppstein
Copy link
Author

@matthew-dean :scope is a pseudo-class that matches the root element of a stylesheet's scope. To me, the css class attribute feels like good place for that association, but certainly another attribute could be used (E.g. <div id="markdown-container" markdown:css>). Basically we have to decide what :scope matches when a stylesheet isn't being used in the context of a web component, but it's the same concept as matching the root element of a web component.

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...

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.

why attach the namespace to an arbitrary attribute, instead of defining an attribute where one can attach (select) namespaces

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 <button app:class="my application classes" bootstrap:class="btn btn-primary"> is more clear and quite enticing.

I think also XML namespacing on an attribute isn't supposed to define the tree behaviors, is it?

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.

@matthew-dean
Copy link

matthew-dean commented Aug 26, 2019

I'm not sure what this means. I'm not defining any tree behaviors that I know of.

What I mean is, in the above example, if markdown:class affects the namespace targeting of the child <p> element, that's not how namespaced attributes were originally defined AFAICT.

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 xmlns of course, since that may still be valid to declare.) That is, you set the name space on the tag, not via a namespaced attribute.

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>

@matthew-dean
Copy link

matthew-dean commented Aug 26, 2019

Re: this

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 <button app:class="my application classes" bootstrap:class="btn btn-primary"> is more clear and quite enticing.

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.

@chriseppstein
Copy link
Author

chriseppstein commented Aug 27, 2019

What I mean is, in the above example, if markdown:class affects the namespace targeting of the child

element, that's not how namespaced attributes were originally defined AFAICT.

Ah, the confusion here is because I wasn't proposing that <p> would be namespaced or that the root element's tagname or other attributes were in the namespace of the stylesheet. That's why I used a descendant combinator (:scope p) which is selecting <p> in the html namespace that is a descendant of an element that matches the :scope.

however, I would still only expect that to have an effect on that particular element.

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 p { ... } in it, it would match every <p> tag in the document.

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.

@ByteEater-pl
Copy link

ByteEater-pl commented Feb 13, 2020

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 class="my-component" here.) Example:

<head>
<title>Example</title>
<style>
@scope my-typography { /* Since it's more general, the at-rule could be named differently. */
    font-family: my-brand;
/* Some stuff from [css-nesting] would be very useful here. */
}
/* The above part could be in a different resource. */
.my-component {
    include-scope: my-typography;
    exclude-scope: x y;
}
</style>
</head>
<body>
<div class="my-component"/>
</body>

Both include-scope and exclude-scope take lists of scopes (variables). They could be extended with features considered above (only(), all as a special name, logical connectives). Styles are attached to a root with include-scope and blocked (from some ancestor root or the global scope with all, that pertains to inherited properties and those set on descendants with [css-nesting]) with exclude-scope. (If a name occurs in computed values of both on the same target, it's as if it wasn't there for styling purposes. Only descendants can re-enable.) This solves (as far as I understand it) the lower boundary issue.

@ByteEater-pl
Copy link

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants