[go: nahoru, domu]

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] Reference selectors #3714

Open
justinfagnani opened this issue Mar 7, 2019 · 35 comments
Open

[css-selectors] Reference selectors #3714

justinfagnani opened this issue Mar 7, 2019 · 35 comments

Comments

@justinfagnani
Copy link

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

$foo {color: red;}

app.js

import {foo} from './styles.css';
document.querySelector('#app').cssReferences=[foo];

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 $:

$foo {
  color: red;
}

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:

$foo, .warning {
  color: red;
}

The important distinctions from other selectors are that:

  • References do not match elements
  • References are exposed on Stylesheet objects

Another important feature is that references are lexically scoped to a CSS file. $foo in a.css is a different reference from $foo in b.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 of CSSReference. (TBD: are they opaque, or do they contain their declarations? Can they be dereferenced via the StyleSheet?)

// define a stylesheet
let stylesheet = new CSSStyleSheet();
stylesheet.replaceSync(`$foo {color: red; }`);

// get the reference
let fooRef = stylesheet.references.foo;

References are also exported from CSS Modules:

import styles, {foo} from './styles.css';

styles instanceof CSSStyleSheet; // true
foo instanceof CSSReference; // true
styles.references.foo === foo; // true

CSS

References are in scope for the file they're defined in.

They can also be imported with @import:

@import {foo} from './styles.css';

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:

<style>
$foo {color: red;}
</style>
<div css-refs="$foo"></div>

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 on Element:

import {foo} from './styles.css';

const div = document.createElement('div');
div.cssReferences = [foo];

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:

import {foo} from './styles.css';

.warning {
  @include $foo;
}

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, are var()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. Participation in the module graph
  2. Exporting classes from stylesheets
  3. Scope emulation & simplifying the cascade
  4. Static analysis

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

@developit
Copy link

This is super interesting! As I read I might just be missing it - what's the rationale for having an opaque type for CSSReference rather than a String / id?

@justinfagnani
Copy link
Author

@developit mainly that references are supposed to be real references to CSS objects. I suppose they could be values of type CSSStyleDeclaration.

@EisenbergEffect
Copy link

Is there any real use for this outside of React-based CSS in JS frameworks?

@giuseppeg
Copy link
giuseppeg commented Mar 7, 2019

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

@EisenbergEffect
Copy link

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?

@EisenbergEffect
Copy link

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?

@justinfagnani
Copy link
Author

@EisenbergEffect The CSS Modules proposal is the most minimal and obvious semantics, immediately usable with adoptedStylesheets. It's intended to be useful on its own now that we have Constructible StyleSheets, and the starting point for other features that are common in various userland CSS modules-like tools. The exact semantics of the css-modules libraries, like exporting only an object with properties associated with class names, aren't sufficient when we need to get an actual StyleSheet object from a module. That's an important difference, but the major intent of the idea - importing a .css file from a JS module - remains. Discussing that is probably best on the CSS Modules issue.

Regarding references vs classes, the situation is similar. The usage of class names in the css-modules library is sometimes very much like references. See the discussion of composition in the README. There class names have restrictions (you can only compose a class name if it was used as a simple selector with only that class name) that make them function very closely to references as proposed here.

It's important to note that the css-modules library is only one of many similar CSS tools. Some are meant to be used with inline styles. Libraries like Glamour abstract over classes entirely. styled-components goes even further and abstracts away the component/style separation. No proposal is going to look exactly like all of the existing userland solutions, and might have different mechanisms, but we can find and solve for the common use cases.

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.

@EisenbergEffect
Copy link

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.

@giuseppeg
Copy link
giuseppeg commented Mar 8, 2019

@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 Object.assign I imagine that, when applied, a bunch of @include $ref would be similar to Object.assign(ref1, ref2, ref3).

If you want to see a PoC, I built an experimental library last year. You can read about it in this blog post.

@bkardell
Copy link
Contributor
bkardell commented Mar 8, 2019

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

  • It seems like you need to be able to indicate in HTML itself that this thing applies over here. Is that not correct?
    • It seems like the value of that would have to be DOMTokenList?
    • Then we create another way to assign 'references' that interact with this via a diff API somehow? Do they reflect or ...?
    • At the end of all of this, it seems CSS is/can still act on those and the cascade and all kind of still plays into it? It seems you could totally write [css-refs="$foo"] in your stylesheet or qSA?

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?

@giuseppeg
Copy link
giuseppeg commented Mar 8, 2019

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

@bkardell
Copy link
Contributor

@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?)

@giuseppeg
Copy link

@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. @reference :--foo or a custom at-rule. Either ways it seems that JS would be a requirement for those and they aren't SSR-friendly. One of the reason why people still prefers CSS in JS over Shadow DOM is that the former can be pre-rendered on the server.

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.

I agree and would like to know the answers to those questions too :)

@matthewp
Copy link

References may not be combined with other selectors

Why not? I would think $foo > .thing would be a useful thing to do. It seems the only alternative is to create another reference. Does $foo > $bar work? It sounds like it would not. So how do you mimic the child combinator (or sibling combinator or many other such useful CSS selector types)? I can only imagine that you need to imperatively add the $bar reference in JavaScript (only on immediate children of $foo).

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 import might be a better way of achieving a similar thing. It would allow you to use another stylesheet and mix it in without having its selectors applied globally.

@justinfagnani
Copy link
Author

Why not? I would think $foo > .thing would be a useful thing to do.

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.

@matthewp
Copy link

I understand how it works but not the why. What is the purpose of this restriction?

@bkardell
Copy link
Contributor

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.

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?

@giuseppeg
Copy link
giuseppeg commented Mar 19, 2019

What is the purpose of this restriction?

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

If we had done mixins would they have just been valid here automatically and we'd be done?

Indeed references are mixins that can be also consumed in JS

@matthewp
Copy link

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

@threepointone
Copy link
 $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;
    }
  }
}

@threepointone
Copy link

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.

@matthewp
Copy link

@threepointone

this would throw an error. you can't define multiple references with the same name in a module.

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?

@giuseppeg
Copy link
giuseppeg commented Mar 19, 2019

@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 const than functions (mixins)

@justinfagnani
Copy link
Author
justinfagnani commented Mar 19, 2019

@matthewp

I understand how it works but not the why. What is the purpose of this restriction?

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 CSSStyleDeclarations (so I should remove the CSSReference opaque type from the issue).

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 CSSStyleDeclarations and contain selectors, critically pseudo-class selectors.

@EisenbergEffect
Copy link

@justinfagnani CSSStyleDeclaration makes a lot of sense to me.

@matthewp
Copy link

@justinfagnani It's still not clear to me. Is this a CSSStyleDeclaration because of a technical constraint? If so, what is the constraint? If not, what is the reason for wanting it to be a CSSStyleDeclaration vs. a CSSStyleRule?

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

@Jamesernator
Copy link
Jamesernator commented May 9, 2019

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 {
  ...
});

@guybedford
Copy link
guybedford commented Jun 25, 2019

Coming to this one a little late here, but some initial thoughts -

  1. I tend to agree with @matthewp that allowing these references to be used in selectors would be preferable. Could we not have a rule that when a reference is applied to an element, the stylesheet is automatically attached to the first stylesheet context wrapping that element (eg shadow dom / document?). Would this perhaps get around some of the apply issues for composition mentioned in having standard selector composition rules over inline composition rules here?

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?

  1. For minification of reference names, I'm a little concerned about exposing stylesheet.references globally as that can then be referenced in non-analyzable in ways that will break under minification. I would much prefer to to keep references to module references only to ensure that references are encapsulated in a way that minification is not likely to break applications.

  2. ES modules have the ability to avoid reference collisions by using import { x as y }. Ideally it would be nice to support @import {foo as bar} from './styles.css'; so that reference name collisions can be avoided with a similar encapsulation. It would be worth checking these types of encapsulation problems further against optimization approaches, and it would be super interesting to get some bundling tooling experiments going here too.

Edit: Update - The counter to (1) above seems to be that nesting would be supported from this comment:

Nesting will allow for these objects to still be CSSStyleDeclarations and contain selectors, critically pseudo-class selectors.

But @matthewp doesn't seem to agree:

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

Would be interesting to hear the verdict on that.

@chriseppstein
Copy link
chriseppstein commented Jun 25, 2019

I think there's three ideas here and they're worth calling out as distinct concepts.

  1. There's a lexically scoped identifier that belongs to the stylesheet and can be used in selectors.
  2. There's a way to write selectors in a stylesheet with a specificity of "inline styles".
  3. There's a way of getting a collection of declarations from a single stylesheet so that they can be assigned to an element without forcing that element to match a specific html structure.

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 $ref:hover or to provide responsive amendments to $ref via media/element queries, which is a major annoyance when working with inline styles.

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 :is() pseudoclass, their use in a ruleset could be assigned to have other specificities. Alternatively, we could introduce specific syntax to author a reference class or a reference id. E.g. (#$foo, .$foo) the values of which could be assigned to the id or classlist of an element through a javascript reference.

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 @extend... I think what I've described here bring reference selectors even closer into alignment with the placeholder selector (Due to implementation constraints, Sass's placeholder selector assumes the specificity of the subject selector that applied it, a CSS-based solution isn't bound by this).

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.

@argyleink
Copy link
Contributor

just stumbled upon this postcss plugin by @morishitter https://github.com/morishitter/postcss-ref, which has some interesting ideas related to this one 👍

@ByteEater-pl
Copy link
ByteEater-pl commented Feb 14, 2020

Inline styles can't style descendants.

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.

@ByteEater-pl
Copy link

The restriction of visibility only in the current stylesheet could be lifted if a disambiguation mechanism for imported names was added, e.g. @import qualified à la Haskell.

@trusktr
Copy link
trusktr commented Apr 3, 2021

There are two separate features blended into one in this proposal:

  1. Ability to export refs to JS
  2. Ability to have unique refs in CSS

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 @function $foo ... would be syntax for it with lexically scoped naming, and exporting it would be separate with @export, just as with ES Modules.

@Jamesernator
Copy link
Jamesernator commented Apr 4, 2021

In that example, there is a clear distinction between lexical naming, and things that are being exported.

I'm generally a fan of export, my example was mostly just illustrating extending $name beyond simply selectors, and instead to wider range of CSS things.

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.

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 CSSStyleSheet and all it's related APIs all these objects are going to have to be exposed to JS anyway, so for example we'd be able to see @keyframes $myAnimation {} as a CSSKeyframesRule. In which case we need all the serialization and stuff to work anyway, so we might as well just allow someElement.style.animationName = myAnimationRule.


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 @import { $name } from "./other.css"; in CSS would basically be sugar for import(...) that allows for removing tedious duplication in cases where the same name is used many times.

@Crissov
Copy link
Contributor
Crissov commented Jan 29, 2023

Utility

There 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. .bg-green {background-color: #97EE11}. Those predefined utility classes form a restricted or controlled set of design options and are applied directly in the markup as usual, so class attribute will frequently have a long list of values. If such lists repeat, the resulting pattern may be condensed in a classic semantic class again by framework-specific means or this can be left to some solution for templates.
This approach apparently has some benefits for implementing graphic designs as websites and maintaining them.

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?

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