Skip to content

Instantly share code, notes, and snippets.

@threepointone
Last active June 18, 2019 08:31
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save threepointone/61e990b450712cfd7dd0bb87ed0c2982 to your computer and use it in GitHub Desktop.
Save threepointone/61e990b450712cfd7dd0bb87ed0c2982 to your computer and use it in GitHub Desktop.
For Nicole

@stubbornella - I’m interested in what browsers can do to support CSS in js as a first class option. Maybe nothing? But it’s something I’ve been background processing.

I'll try to be brief, but please ask me to go into more detail about any bits don't seem right to you. I haven't spent a lot of time thinking about the edge cases, but I'm drawing on my experience with various css in js libs to make assumptions about this model covering most of them.

The elevator pitch - what features should css have, to obsolete all css in js libraries? Further, how do we do it with minimal change to the language itself?

The proposal introduces a few new ideas

  • references
  • extends keyword
  • a javasript api

references

ok, so first, a new type of selector. I like calling it a 'reference', but we could also call it a 'module', a 'marked selector', or anything else. you can declare it in the top scope like so -

// styles.css
$xyz { color: red; }

this references is local to the css file it's declared in; another file could have another reference named xyz and it wouldn't clash with this.

flesh this out a bit, with syntax familiar to css developers

$xyz { color: red }
$xyz:hover { color: blue }
$xyz:hover .child { color: green }

the above 3 rulesets can be considered to be part of the same reference xyz

references can not be used as a part of other selectors

$xyz { color: red }

// ERROR!
$abc $xyz { color: blue }

extends

you can compose different modules with a syntax similar to css modules

$xyz { color: red }

$abc {
  extends: $xyz;  
  font-weight: bold;
}
// somewhat equivalent of writing `class="xyz abc"`

a unique feature is to compose within selectors

$def { color: green }
$def:hover { extends: $xyz; font-style: italics }

we extend the @import statement to expose references, letting us compose across file boundaries

// more-styles.css
@import 'styles.css' as {xyz}; 

$def { color: green }
$def:hover { extends: $xyz }

I wrote a javascript version of the above rules in https://gist.github.com/threepointone/9f87907a91ec6cbcd376dded7811eb31, which underpins the composition model of glamor/emotion libraries.

js

while composing in css files gets a lot of mileage, most interesting usecases are happening inside js files / wherever your components are defined. eg - <div css={[defaultStyle, props.isSelected && selectedStyle]}>...

on the javascript side, you should be able to 'natively' import from a module

import {xyz, abc} from './styles.css'
// this assumes "css as modules" are a thing. related - https://github.com/sebmarkbage/ecmascript-asset-references

these references expose 2 functions

  • ref.concat(ref1, ref2, ...) : composes references, and returns a new reference. much like array.concat(...)
  • ref.toString() - if, and when serializing to html; allowing for progresive enhancement and avoiding the fouc from a js-only solution

these references can be applied to an element, let's say as a css prop

function App(){
  return <div css={xyz}>
    hello <span css={xyz.concat(abc)}>world</span>
    {/* alternately */}
    hello <span css={[xyz, abc]}>world</span>
  </div>
}

// `xyz.concat(abc)` would be equivalent to 

// $def {
//   extends: $xyz;
//   extends: $abc;
// }

// and returning the `$def` reference 

that's... mostly it. with these few rules, we could get the core primitives that most css in js libraries rely on, significantly alleviating concerns regarding the same.

some nice consequences of this model

  • it's a superset of existing css, so there's very little learning curve, keeping it accessible.
  • explicit scopes, which is just best in life.
  • the order of loading modules doesn't matter, letting us load files asynchronously without worrying about the cascade order
  • can efficiently polyfill this for 'older' browsers; (indeed - it's functionally equivalent to glamor's css() function)
  • friendly to dead code elimination; references that aren't, er, referenced, can be deleted from your production bundle
  • trivial to do critical css extraction; by parsing out references from html, you can calculate the minimum amount of css required for that page
  • some other nice compile time optimization opportunites open up; eg - we can convert all of it to so called 'atomic css', expressing modules in terms of those primitives, resulting in reeeeally tiny css bundles
  • my favourite bit is how this amplifies the power of 'traditional' frameworks like oocss and itcss. you can design, compose, and use entire design systems with references, but only ship the bits that are used, completely automated. great for letting teams use whatever workflow they're comfortable with.

some open questions

  • how would this look in a react native world? just remove :pseudo selectors?
  • what is the SSR story? I did the hand-wavey thing over toString(), but letting the html and css load and be styled without waiting for the js bundle to load is key for great ux.
  • how does this interact with existing css? consider specifically <div css={xyz} className="something"> - which styles take precedence? imo references take precedence over regular selectors (but maybe not?)
@stubbornella
Copy link

@ojanvafai @chrishtr can you check this out? This is the gist about css in js I was talking about.

@developit
Copy link

Hiya! Some thoughts:

references: seems like a more abstract version of :host. Any chance theres a way to combine the two?

import references: this reminds me of composes, but with a much nicer syntax.

general: we're just about to ship Constructable Stylesheets, which make it possible to instantiate a CSSStyleSheet object for a given stylesheet as a string. It also makes it possible to mutate the sheet later on (via CSSOM or by replacing its source text), and updates get propagated out to all roots where the sheet has been applied. There's new methods that allow waiting for stylesheet (sub)resources to load, which solves FOUC for lazily-applied styles since assigning a constructed stylesheet to a DOM subtree is then instantaneous.

So I'm wondering: is there a middle ground between all these things? something like:

import sheet from './styles.css';
sheet instanceof CSSStyleSheet; // true

function App(){
  return <div sheet={sheet}>
    hello <span class={sheet.rules('xyz', 'abc')]}>world</span>
    {/* alternately, via JSX preprocessing: */}
    hello <span css={[xyz, abc]}>world</span>
  </div>
}

Notable perks to an approach like this:

  1. CSSStyleSheet seems like a pretty intuitive return value for import '.css', and is already spec'd.
  2. updates (eg: HMR) would be super easy. Webpack can automate too: module.hot.accept('style.css', r => sheet.replace(r('style.css')))
  3. We're missing an API for getting matching rules from a sheet (querySelector for CSSRules). Maybe that plays into the idea of obtaining references to rule (names) for DOM assignment?

@threepointone
Copy link
Author

I’m so happy to see y’all here :) out for dinner, so I’ll get to this in a few hours Jason. Thanks for the comments! Will respond

@justinfagnani
Copy link

FYI, my CSS Modules proposal: WICG/webcomponents#759

@developit
Copy link

@justinfagnani
Copy link

@developit seems essentially identical to mine. Can you add a voice of support over at WICG/webcomponents#759? The Edge team, Domenic, Kouhei, etc., are aware of it and talking about it and JSON modules in the context of HTML modules already.

@developit
Copy link

developit commented Jan 31, 2019

haha, yeah! I'll chime in there, hadn't seen it before.

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