[csswg-drafts] [css-selectors] Reference selectors (#3714)

justinfagnani has just created a new issue for https://github.com/w3c/csswg-drafts:

== [css-selectors] Reference selectors ==
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](https://wicg.github.io/construct-stylesheets/), and the [CSS Modules proposal](https://github.com/w3c/webcomponents/issues/759).

This is largely based on [ideas](https://gist.github.com/threepointone/61e990b450712cfd7dd0bb87ed0c2982) from @threepointone, the author of [Glamour](https://github.com/threepointone/glamor).

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
```css
$foo {color: red;}
```

app.js
```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 `$`:

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

```css
$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?)

```js
// 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:

```js
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`:

```css
@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:

```html
<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`:

```js
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:

```css
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](https://www.xanthir.com/b4o00) (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](https://github.com/w3c/webcomponents/issues/759).

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

Please view or discuss this issue at https://github.com/w3c/csswg-drafts/issues/3714 using your GitHub account

Received on Thursday, 7 March 2019 01:24:11 UTC