-
Notifications
You must be signed in to change notification settings - Fork 668
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] Reference selectors #3714
Comments
This is super interesting! As I read I might just be missing it - what's the rationale for having an opaque type for |
@developit mainly that references are supposed to be real references to CSS objects. I suppose they could be values of type |
Is there any real use for this outside of React-based CSS in JS frameworks? |
@EisenbergEffect AFAIK CSS Modules are language agnostic i.e. you can use them with other languages (eg. PHP). @justinfagnani I ❤️ this. FWIW a while ago I had a similar idea but thought of another syntax https://twitter.com/giuseppegurgone/status/1089633480458358785 just posting it here for the record. |
But with CSS Modules, does not the module export an object with properties for each class? Why not define that as the standard behavior? Why add an additional construct? It seems that the main feature is the lexical scoping of the references, but why not define that that's the way classes work when imported through the module system? |
To clarify, I'm connecting this to the CSS Modules community spec that people use today, not the W3C spec linked above. But my question is...why not align them? Why don't we make the w3c CSS modules work like the CSS Modules that people use through transpilers? |
@EisenbergEffect The CSS Modules proposal is the most minimal and obvious semantics, immediately usable with Regarding references vs classes, the situation is similar. The usage of class names in the It's important to note that the For this proposal I see the use cases having the common thread of directly referencing a declaration, whether that's for SASS-like includes (or css-modules-like composes), or directly styling an element. I think modeling this as explicit references, rather than trying to repurpose classes to that effect, has major benefits in terms of understandability, and likely performance: if you only use a class as a unique identifier for a declaration, and the developer can directly associate elements and/or other declarations, then don't bother trying to match anything else against it. |
That all makes sense. Thanks for expounding a bit. I think this particular notion of references might be more useful when trying to accomplish scoped styles without shadow dom. It seems a bit less useful with shadow dom. With that in mind, it would be cool to see a PoC of css modules (the library) built on top of CSS Modules (the spec) using the reference idea proposed here. If that worked out nicely and this worked for things like Glamour too, then I could get behind it. To be honest, I was always frustrated that shadow dom seemed to mix so many concerns together: slotted composition, scoped styles, dom encapsulation, event retargeting. If this provides a way to extract out the ability to scope styles without using shadow dom, that's a win I think. |
@EisenbergEffect I think that one thing that is quite different from classical ShadowDOM encapsulation is that references don't obey to cascade or specificity, you can mix them together and get a final predictable result. If you think of a reference as a JavaScript Object and if you are familiar with JavaScript's If you want to see a PoC, I built an experimental library last year. You can read about it in this blog post. |
Really, being able to discuss some of this seems to, in my mind at least, hinge on what it would actually mean to associate an element with a reference and why/how you might do that in practice, but almost all of these have inline questions in @justinfagnani's original post. Would it be productive to talk details on some of those things? I feel like it is sort of impossible to even ask good questions with only vague ideas on what seem like kind of ultimately maybe the most critical points. It's also possible I am missing something important here and over-stating, but... for example.. If I had /* styles.css */
$foo { color: yellow; }
$foo { background-color: blue; }
@media only screen and (max-width: 480px) {
$foo {
background-color: black;
}
} and /* app.js */
import {foo} from './styles.css';
document.querySelector('#app').cssReferences=[foo]; and <style>main { background-color: purple; }</style>
<script type="module" src="app.js"></script>
<main id="app">
Hello
</main> Can someone explain what color do we expect the foreground and background to be, and why? If there were agreement on that that I could wrap my head around, I feel like this would be a little easier to discuss. Also, at the end of this
Is it possible that classes + some things in other proposals could be 'enough'? Like maybe stuff in https://tabatkins.github.io/specs/css-aliases/ provides interesting ideas? If you can alias classes with pseudo-names, we could maybe both export those and have consistent specificity, and use that to also chase the 'how to match' end of this as well, all in one? |
@bkardell the moment you introduce classes (selectors) and the cascade you can't resolve references in application order anymore. I think this proposal is about providing that feature instead. Somewhat similar to this and what I explained in this tweet. |
@giuseppeg As I said tho, it seems to me that selectors and the cascade inevitably are still there and that means we'd have to figure out where all this fits. To me, it feels very hard to discuss without sorting some of the things I asked above - these and more are kind of listed as '..?" unknowns in the opening post. (note: you said tweet, but linked to codesandbox - can you update that for posterity/clarity here?) |
@bkardell oh yes sorry, I fixed the links. I meant to comment on using css aliases (selectors) to define references. I am not sure how one would enforce "consistent specificity" and how we would distinguish between regular class aliases and references. Unless you meant a new extension eg.
I agree and would like to know the answers to those questions too :) |
Why not? I would think I don't think CSS should adopt features that render other useful CSS features to be disabled. If selectors are a problem then we should improve selectors while still keeping them around. I agree with some of the others that |
Think of references as like inline styles, but parsed once even if the same rules are used on multiple elements. Inline styles can't style descendants. |
I understand how it works but not the why. What is the purpose of this restriction? |
Hmm, that's an interesting point of view and seems like a good analogy. If we had done mixins would they have just been valid here automatically and we'd be done? |
@matthewp the purpose is deterministic styles resolution based on application order - not cascade or specificity* * it is possible to achieve deterministic styles resolution when specificity is of a particular kind. I implemented a solution that resembles references (a CSS Modules/Blocks hybrid) as a PoF, it is called DSS and you can read about it here. From my experience here are the CSS features that can work in such a system.
Indeed references are mixins that can be also consumed in JS |
@giuseppeg I think some amount of specificity is unavoidable. This is specificity: $foo { color: blue; }
$foo { color: red; } And what of @bkardell's example with a media query? Are references not allowed to be used inside media query blocks? It seems like you can use references in a way that avoid specificity just by not using complex selectors, not using media queries, etc. But this is an orthogonal problem, some sort of reference idea has value in normal CSS where you do use selectors. |
$foo { color: blue; }
$foo { color: red; } this would throw an error. you can't define multiple references with the same name in a module. @bkardell's example would be rewritten as $foo {
color: yellow;
@media only screen and (max-width: 480px) {
& {
background-color: black;
}
}
} |
sorry I haven't piped in here yet, been swamped. I'll try to get some time next week and try to answer some questions + thoughts. cheers all. |
This is very un-CSS like, I'm not sure I've ever seen CSS throw, even with bad syntax mistakes. This would mean rules defined after this error would not be applied, which again is not CSS like (usually it continues to apply rules further down the stylesheet when encountering a syntax error). I still don't have a clear answer as to why this specificity requirement is tied to the reference idea. Am I wrong that you cannot achieve what you want simply by not using complex selectors? |
@matthewp, @threepointone I am not sure about that. There is definitely smarter people than me in this thread to make the final call but I'd probably expect that to be doable but scoped to the current module only - after all that's possible in JS function foo() {
return `color: red`
}
function foo() {
return `color: green`
}
console.log(`
.foo {
${foo()}
}
`) or with Sass mixins @mixin foo() {
color: red;
}
@mixin foo() {
color: green;
}
.foo {
@include foo()
} The only difference from classic cascade is that the last one overrides the previous completely. edit it would be nice to avoid this and throw an error as @threepointone suggested though. Removing the ordering factor is one of the main goal of this proposal after all. References are more like variables declared with |
I think the question for me is: what is a reference a reference to? In talking to @developit in this thread and @tabatkins offline, I think to start with in this proposal these are references to This means that they aren't strings or selectors, and can't be combined with such. Nesting will allow for these objects to still be |
@justinfagnani |
@justinfagnani It's still not clear to me. Is this a Also, nesting has been advertised as being purely sugar (maybe the thought here has changed). If so you wouldn't be able to do nesting in a |
This idea of lexically scoped names in CSS would be really powerful for other potentially name-colliding things too e.g.: Values$iconSmall: 20px;
$iconMedium: 50px; FontFaces/KeyFrames@keyframes $fadeIn {
...
} Scoped Properties/* fancy-dialog/styles.css */
@property $titleColor;
h1 {
color: var($titleColor);
} import { titleColor: dialogTitleColor } from 'fancy-dialog/styles.css';
import { titleColor: articleTitleColor } from 'cool-article/styles.css';
container {
$dialogTitleColor: red;
$articleTitleColor: blue;
} // fancy-dialog/component.js
import { titleColor } from './styles.css';
CSS.registerProperty({
name: titleColor,
syntax: '<color>',
}) Worklet Names@name $masonry;
foo {
display: layout($masonry);
} import { masonry } from './styles.css';
registerLayout(masonry, class MasonryLayout {
...
}); |
Coming to this one a little late here, but some initial thoughts -
I do worry otherwise about how easily it would be to end up with templates looking like: import { styleA, styleB, styleC } from './style.css';
import html from 'template';
export default html`
<div cssReferences=${styleA}>
<h1 cssReferences=${styleB}>
<p cssReferences=${styleC}>
</div>
`; sort of thing, which seems like it would get pretty ugly?
Edit: Update - The counter to (1) above seems to be that nesting would be supported from this comment:
But @matthewp doesn't seem to agree:
Would be interesting to hear the verdict on that. |
I think there's three ideas here and they're worth calling out as distinct concepts.
Reference selectors as specified in this proposal sit at the intersection of these ideas and make them fairly easy to implement, but they also introduce constraints that I feel are surprising and out of step with other selector constructs (namely that there must be a singleton ruleset for each reference). Specifically, I think it's a miss if you can't author something as simple as If we allow lexically scoped reference selectors to participate in the local cascade (with a specificity of "inline styles") then we can assign that identifier to an element through javascript as envisioned above. The rulesets using that selector as the subject selector will then match it with the semantics of having been assigned inline, but more nuanced cascade and document refinements can also occur and the descendants of that element can be targeted through their own reference assignments or through other dom-based selectors. I think reference selector should have a default specificity of "inline styles" but through proposals like the With this approach, we can also assign reference selectors to elements through non-javascript means: For example, another selector can be used to apply a reference to elements matching the document query, allowing a form of composition that would still be resolved via the cascade. To address concept 3 above, I think there's value in being able to ask a single stylesheet to construct a reference based on a "query of the styles". This could preserve some of the interactive aspects like pseudoclasses and media/element queries, while resolving the document structure specific bits. Then this generated reference could be assigned to any element regardless of that element's HTML structural properties. I admit this idea is a little half-baked right now, but I really like the idea of being able to take styles from a single stylesheet and apply it to elements that it wouldn't otherwise match. Lastly, I think we should give Sass's placeholder selector a nod. It's basically a generalized reference selector that can be applied by another selector via Update: @bkardell asks: "Before or after actual inline styles? Or if you use one the other doesn't apply?" Ideally it would be based on assignment order to the element. If that doesn't fly, I suppose one has to win... probably the actual inline styles. |
just stumbled upon this postcss plugin by @morishitter https://github.com/morishitter/postcss-ref, which has some interesting ideas related to this one 👍 |
They will if [css-nesting] is adopted. Also previous drafts (so possibly future specs could bring that back) of the style attributes CSS spec had a few profiles from which host languages could choose, the more comprehensive ones included pseudo-classes, at-rules and much more. |
The restriction of visibility only in the current stylesheet could be lifted if a disambiguation mechanism for imported names was added, e.g. |
There are two separate features blended into one in this proposal:
Perhaps it would be better for the language to make those entirely separate concepts. @Jamesernator's comment shows how lexically scoped constructs could be useful in CSS itself, but may diverge from the ability of exporting these things to JS because they ref a variety of things, not just style rules. With the current proposal, every ref is exported, which is not desirable if a CSS author would want refs to be private to a module (like a local variable in a JS module). The naming feature is currently directly coupled to the exporting feature. On the JS side, it seems the main thing needed is rules, and applying those rules in order. That is a subset of features that could be achieved with lexically-scoped references in the CSS language itself. Perhaps first we need a clear description of what is allowed to be exported to JS, and what stays in CSS, and syntactic differences for those. Maybe "refs" are merely lexically scoped names like @Jamesernator hinted at, and we need specific constructs for exporting rules or anything else. Here's an example that shows the concept of exports decoupled from lexically-scoped naming of features: @import {$aRule} from "./somewhere.css"
@export @rule $someRule {
@merge $aRule;
color: cyan;
}
@rule $otherRule {
color: deeppink;
}
@export $otherRule;
/* not exported */
@rule $anotherRule {
color: yellow
}
.foo p {
@merge $aRule;
@merge $anotherRule;
background: url(...);
} In that example, there is a clear distinction between lexical naming, and things that are being exported. I think such a distinction can lead to better understanding from one CSS author to the next, and to more robust definition of particular spec features in such a way that new features have a more clear way of being added later in terms of language. For example, if a functions-in-CSS feature (without requiring support from JS) ever came out later (not saying that it should) then |
I'm generally a fan of export, my example was mostly just illustrating extending
I think most would be useful, but rules would certainly be the most important to support as you could wrap most of the others in a rule. Although note that in either case, because we have Given that serialization needs work, I wonder if a good way to do this would be make css imports/lexical names also have a "dynamic" syntax, in way of a function. In this case we'd have: @keyframes $localAnimation {
}
.myElement {
@merge import(./path/to/module.css $myImport);
animation-name: import(./animation.css $myAnimation);
}
.otherElement {
/* Local behaves unchanged */
animation-name: $localAnimation;
} That way from JS, these would be equivalent: import { myAnimation } from "./sheet.css";
console.log(myAnimation); // CSSKeyframesRule
myElement.style.animationName = myAnimation;
// This would get serialized to the import function form:
// import(https://my.tld/path/to/sheet.css $animationName)
console.log(myElement.style.animationName); const cssModule = new URL("./sheet.css", import.meta.url);
myElement.style.myAnimation = `import(${ cssModule.href } $myAnimation)`; Using |
UtilityThere are so-called utility-first frameworks like Tailwind, which are based on atomic classes that often contain a single CSS rule and also somewhat expose property and value in their class name, e.g. If I understand the proposal correctly (and I certainly don’t understand all of it), a part of it would reenable the separation of style and content within the utility first paradigm: $utility {color: green}
.pattern {@include $utility} <a style="@include $utility">foo</a>
<b class="pattern">bar</b> instead of current non-standard solutions like .utility {color: green}
.pattern {@apply utility} <a class="utility">foo</a>
<b class="pattern">bar</b> Is this corollary correct and is this in scope? |
This proposes a new type of selector*, tentatively called a "reference", that serves as a way to uniquely identify a declarations block.
References are intended to enable a number of features and use cases addressed by CSS-in-JS type libraries, in a way that's incremental to existing CSS patterns and coherent with the CSSOM, Constructible Stylesheets, and the CSS Modules proposal.
This is largely based on ideas from @threepointone, the author of Glamour.
Disclaimers:
The description of this idea may seem fairly concrete, but it's largely speculative. The important parts are the concept of references and the use cases they enable. There may be ways to achieve the similar results with new constructs that use classes, for instance.
*"selector" may be a bad categorization, as references do not actually select any elements, but reference a declarations block instead. They appear in the selector list of a ruleset though, and may be used to associate an element with one or more declaration blocks.
Quick Example
styles.css
app.js
The app's element is now styled with red text.
CSS Syntax
A reference appears in a selector list, and is denoted by a sigil prefix, tentatively
$
:References may not be combined with other selectors (but they may need to support pseudo-classes, ala
$foo:hover
), but they can appear in a selector list:The important distinctions from other selectors are that:
Another important feature is that references are lexically scoped to a CSS file.
$foo
ina.css
is a different reference from$foo
inb.css
.Getting References
Once a reference is defined in a CSS file is must be imported into another context to be usable. The web has three main types of contexts: HTML, CSS, and JavaScript;
JavaScript
CSS references are exposed on
CSSStyleSheet
, which allows us to get them from styles defined in<style>
tags, loaded with<link>
, etc. References are instances ofCSSReference
. (TBD: are they opaque, or do they contain their declarations? Can they be dereferenced via the StyleSheet?)References are also exported from CSS Modules:
CSS
References are in scope for the file they're defined in.
They can also be imported with
@import
:Here we're borrowing JS-like syntax and putting named imports before the URL, since media queries come after the URL. I'm not sure if this syntax works.
HTML
A key use-case for references is to enable tools to statically analyze and optimize CSS. For this reason they are intended to be relatively opaque, easily renamable, and not accidentally meta-programmed over. This may be in tension with a string representation usable in HTML. But with HTML Modules being proposed, and that bringing some kind of scoping to HTML, and HTML into the module graph, it may be useful:
An open question is: is this any different from classes, especially if we generate unique classes with a library? The answer may be no, and that in general this isn't a good idea, but that we need some HTML representation for server-side rendering. TBD.
Using References
Styling Elements in JS
Since references do not match elements, declarations identified by references must be associated with elements directly. via a new
cssReferences
property onElement
:Cascade
Q: Would references introduce a new cascade order?
Composing Declaration Blocks in CSS
This may or may not be a good idea, but is an example use case.
CSS developers often want to compose styles. SASS, etc., allow this, as do many CSS-in-JS libraries and the now defunct
@apply
proposal.One problem with
@apply
was its dynamically scoped nature. While useful in some cases for subtree-scoped theming, most programmers are use to lexical scoping and simply wanted to import a "mixin" and apply it directly, and passing the custom variables down the tree had too much overhead.References allow us to have a more lexically-scoped version of
@apply
, let's call it@include
as a nod to SASS:This functions more like SASS
@include
, but not quite because$foo
isn't a mixin with parameters. This idea may also suffer from the problems highlighted in @tabatkins@apply
post (especially, arevar()
s in references resolved early or late?). This whole area deserves its own proposal(s).Rationale
When looking userland CSS frameworks, especially CSS-in-JS libraries, to see what features they add that aren't present natively, I think we see a few major common themes:
(1) is addressed by the CSS Modules proposal.
(2) is important for a few reasons, like ensuring class names are correct, enabling refactoring of class names, dead CSS elimination, etc. It's addressed by references being exported in CSS Modules, and available via the CSSOM.
(3) generally works by modifying or generating class names in CSS to be unique, and sometimes discouraging the use of selectors. With purely generated class names and no other selectors or combinators, you kind of get scoping because styles are directly applied as if using style attributes, and nothing matches across component boundaries.
Shadow DOM generally solves the need for userland scoping, but many developers will prefer the mental model of directly applying styles to an element via a reference, than considering the cascade. References allow these mental models to be intermixed, even within the same stylesheets and DOM trees.
(4) By effectively adding named exports and imports, and exposing references on a
CSSStyleSheet.references
object, and not via a map-like interface, references should make it easier for tools to associate CSS references across file and language boundaries. Developers could use long descriptive reference names, and have tools minify them, enable jump-to-definition, etc. like they do with pure JavaScript references.Lexical vs Dynamic Scoping
JavaScript programmers are use to lexical scoping, but HTML + CSS implement a kind of dynamic scoping: CSS properties and variables inherit down a tree and their value depends on the location of the matched element in the tree, similar to dynamic scoping depending on the call stack. This dynamic scoping is extremely powerful and necessary for many styling techniques (it's even useful in JS, see React's context feature), but sometimes a developer just wants to say "put these styles on this element" and not worry about tree structures or conflicts from other selectors.
The text was updated successfully, but these errors were encountered: