Skip to content

Instantly share code, notes, and snippets.

@threepointone
Last active September 4, 2022 07:43
Show Gist options
  • Star 69 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save threepointone/0ef30b196682a69327c407124f33d69a to your computer and use it in GitHub Desktop.
Save threepointone/0ef30b196682a69327c407124f33d69a to your computer and use it in GitHub Desktop.
why css purists will love glam

I made a little styling lib called glam

(some features are in development)

one

let's start off with the simplest use case. we'll make an 'index.html' page, and assume we've setup our js bundler to output bundle.js

<!DOCTYPE html>
<html>
<head>
  <title>glam</title>
  <link href='index.js.css' ref='stylesheet'/>
</head>
<body>
  <div id="root"></div>
  <script src='bundle.js'></script>
</body>
</html>

let's write some code! make an index.js file -

import css from 'glam'
let cls = css` color: red `

window.root.innerHTML = `<div class='${cls}'>
  hello world!
</div>`

after bundling, we see 2 files have been created -

import css from 'glam'
let cls = 'css-5sd9a'

window.root.innerHTML = `<div class='${cls}'>
  hello world!
</div>`

index.js.css

.css-5sd9a { color: red }

it appears our computer has done what we would have done by hand: extracted the css, and left behind a classname to be used in html/react/ember/whatever. this is... awesome! you don't have to write most selectors now. some other benefits -

  • our setup can spot when the 'same' rules are defined, and dedupe!
  • you can get 'readable' classnames by passing a name property
  • we add some postcss goodies, so you can use nested selectors, automatically add vendor prefixes, etc.
  • You can even run your tools like stylelint, etc on the css file.

With this, you can already do everything plain css/your favorite preprocessor can, and with no runtime cost.

for people with a 'normal' css workflow, this should be enough reason to switch to glam... but wait, there's more!

two - variables

let's try something dynamic

window.root.innerHTML = Array.from({ length: 10 }, (_,i) => i).map(size
  `<div class='${css` font-size: ${2 * size + 1} `}'>
    ${size}
  </div>`
)

this should generate 10 divs, each one with bigger text than the last.

this compiles down to js and css -

window.root.innerHTML = Array.from({ length: 10 }, (_,i) => i).map(size
  `<div class='${css('css-7asd4a', [2 * size + 1])}'>
    ${size}
  </div>`
)
.css-7asd4a { font-size: var(--css-7asd4a-0) }

during runtime, the css function generates classes to be added to the element, so it would looks something like this -

<div class='css-7asd4a vars-687sdf'> 2 </div>
<div class='css-7asd4a vars-6sa76s'> 4 </div>
<div class='css-7asd4a vars-iuuq76'> 6 </div>
...
<div class='css-7asd4a vars-23ka09'> 20 </div>

and it dynamically adds relevant rules to the browser's stylesheet

.vars-687sdf { --css-7asd4a-0: 2px }
.vars-6sa76s { --css-7asd4a-0: 4px }
.vars-iuuq76 { --css-7asd4a-0: 6px }
...
.vars-23ka09 { --css-7asd4a-0: 20px }

again, this is exactly how we would have done it 'by hand' - extract the static portion into a css file, and use a combination of javascript and css variabless for the dynamic parts. We can even prerender the html beforehand to generate the vars-* classes and include in a real stylesheet, if used along with server-side/static rendering. And with newer versions of preact/react, we'll be able to directly set the variable values as inline styles, making it very viable for a high(er) performance UI.

Now, this is where it goes from great to awesome.

css variables are amazingly useful, but they don't have wide enough browser coverage yet; from 50 - 80%, depending on which part of the planet you're on. Further, preprocessors like sass/less/postcss CANNOT polyfill css vars, because they only run at compile time. This usually relegates custom properties to high level theming, being only used to define values in a :root context, and being statically replaced in dependent stylesheets.

However. HOWEVER. We control runtime too. Specifically, we control generation of the selector, and can effectively 'polyfill' css variables by generating fresh classes. So, if you compile the bundle with vars: true as a plugin option, the above would generate -

window.root.innerHTML = Array.from({ length: 10 }, (_,i) => i).map(size
  `<div class='${css('css-7asd4a', [2 * size + 1], (x0) => [`.css-7asd4a { font-size: ${x0}; }`] )}'>
    ${size}
  </div>`
)

during runtime, it would generate the following html -

<div class='css-7asd4a-68sadf'> 2 </div>
<div class='css-7asd4a-6sa76s'> 4 </div>
<div class='css-7asd4a-iuuq76'> 6 </div>
...
<div class='css-7asd4a-23ka09'> 20 </div>

and add the following rules to your stylesheet -

.css-7asd4a-687sdf { font-size: 2px }
.css-7asd4a-6sa76s { font-size: 4px }
.css-7asd4a-iuuq76 { font-size: 6px }
...
.css-7asd4a-23ka09 { font-size: 20px }

with no hacks, we've effectively polyfilled css variables. amazing!

three - composition

css has no real form of composition. further, the cascade is, imo, a Bad Thing, particularly when we load our stylesheets asynchronously, and in non predictable order.

people then assume that adding multiple classes to an element is 'how' you compose different css rules, which works fine-ish until stylesheets grow bigger, and then you battle specificity and selectors and !important... it's all very exhausting.

Thankfully, there appears to be a spec in the works that aims to solve this problem - @apply

With this spec in play, you can define reusable 'chunks' to be expanded in-place inside other css rules. It might look like this -

:root {
  --toolbar-theme: {
    background-color: hsl(120, 70%, 95%);
    border-radius: 4px;
  };
  --toolbar-title-theme: {
    color: green;
  };
}

.toolbar {
  @apply --toolbar-theme;
}

.toolbar > .title {
  @apply --toolbar-title-theme;
}

.warning {
  --toolbar-title-theme: {
    color: red;
    font-weight: bold;
  };
}

Let's rewrite that, it looks nicer in glam

let toolbarTheme = fragment`
  background-color: hsl(120, 70%, 95%);
  border-radius: 4px;`

let toolbarTitleTheme = fragment` color: green `

let warning = fragment`
  color: red;
  font-weight: bold;`

let toolbar = css`
  @apply ${toolbarTheme}
  & > .title {
    @apply { props.warning ? warning : toolbarTitleTheme}
  }`

again, we can expect glam to extract out the static parts to a css file, and use javascript for the rest. however, the lack of browser support is much worse here (as of this writing, zero browsers; chrome works with a flag). BUT. Just like the previous case, because we control the runtime, we can effectively polyfill the @apply spec by passing apply: true as a plugin option. WHich is enabled by default.

AMAAAAZING.

four - the punchline

arguments about 'pure' css vs css-in-js systems miss some points -

  • styling solutions are on a spectrum of tradeoffs, based on different vectors - runtime dependency, compile time optimizations, DX, target platforms, feature support, and so on.
  • most of these solutions have some form of prerendering/server-side support, meaning the load-time cost can be mitigated by sending plain css with the html, and preventing fresh rule creation in runtime.
  • the loading/caching of stylesheets is still an unsolved problem, especially in interactive scenarios. even webpack's css loader, even though it's provided 'pure' stylesheets, embeds them as strings in js bundles so as to precisely load and control the injection of css into the document at its time of choosing.
  • while css-in-js sytems will always be able to implement/fill 'pure' css features (by virtue of using real stylesheets in the background), there are some features of css-in-js system that will never be duplicated by pure css systems: being able to compose styles predictably, extracting and bundling 'precise' css based on the html that used it, etc.
  • in most applications (I'd wager > 95%), your css-in-js solution will not be your bottleneck. I've never seen it. Some solutions might push cheaper mobile devices to the edge, but there's effort underway to mitigate that.

in short, pick whatever you want, be certain you understand exactly what your lib does, and go forward.

nb

  • glam is buggy! it's barely a week old. use any of the other solutions if you want something prod-ready!
  • vars/apply as plugin options don't work yet, use inline: true instead.
@matthew-dean
Copy link

matthew-dean commented May 5, 2017

Further, preprocessors like sass/less/postcss CANNOT polyfill css vars, because they only run at compile time

That's false. Less can compile in-browser! The others can't though.

@threepointone
Copy link
Author

noted, but that's not relevant to this point, which is that it can't polyfill css vars, especially because it's compile time only, even if the compile happens in the browser. (and if you're going to ship the parser/infra to the browser, you might as well use glamor/styled-components/etc etc :)

@jfrolich
Copy link

jfrolich commented May 5, 2017

Amazing indeed. Object notation would make it even more awesome so that the syntax is closer to glamor. Would love to see a marriage of this and glamorous.

@jamesplease
Copy link

jamesplease commented May 5, 2017

@threepointone, thanks for the write up! I'm curious – how do you do contextual styles with this system? i.e.; this component has margin of A usually, but when it is within a particular component, it has margin B. (assuming native CSS Vars aren't available)

With regular CSS + BEM, i'd do something like:

.my-component { margin-bottom: 5px; }

then, in another component's file:

.another-component .my-component {
  margin-bottom: 10px;
}

I find myself needing to do this sort of thing quite frequently, yet I always wonder how css-in-js systems would work with this pattern. When I write CSS, using BEM, everything has the same specificity, and there are no collisions, so the cascade goes from being an unpredictable, possibly scary thing to being a very valuable tool.

Would you recommend exporting the class name from the first component, then importing it and using it to dynamically generate the class name in the other component?

@rauchg
Copy link

rauchg commented May 6, 2017

@jmeas coming from a similar world (styled-jsx, which scopes / obscures the CSS within a component completely), we accomplish that by wrapping the component you're embedding and applying styles to that one.

This is similar to functional composition or decoration. The implementation details of the component you're embedding remain unchanged, you're just applying margin / padding to the component that wraps it.

Another pattern that works is taking advantage of the inherit keyword in CSS, and applying styles to the parent whenever you incorporate that component.

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