I made a little styling lib called glam
(some features are in development)
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!
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!
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.
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.
- 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, useinline: true
instead.
That's false. Less can compile in-browser! The others can't though.