Writing A CSS-in-JS Library From Scratch
--
CSS-in-JS libraries have been getting a lot of attention lately. You’ve probably used or heard of Styled Components, Glamor, or (shameless plug) hopefully Emotion. But have you ever wondered how to build a CSS-in-JS library? As I’m preparing for the release of emotion version 8, I’d like to show you how you can create your own from scratch!
The Basics
In order to write some CSS in a HTML file the <style>
tag is used.
<style>
.circle-avatar {
border: 1px solid darkorchid;
}
</style>
The browser parses the text content and inserts the rules into the CSSOM. Once complete, any elements that match the inserted rules have their associated styles applied to them.
We are going to do the same thing, but with Javascript.
First, we create a <style>
element and append it to the head.
const sheet = document.head.appendChild(
document.createElement('style')
).sheet
The style element has a property, sheet
, that is an instance of CSSStyleSheet
and contains the apis needed for writing CSS with Javascript. The two important apis we need are sheet.cssRules
and sheet.insertRule
.
cssRules
sheet.cssRules
is a live CSSRuleList
, listing the CSSRule
objects in the style sheet. It can be used to access rules you’ve already inserted, but its length
property is more important to us when using the insertRule
method.
insertRule
The core of our entire library is going to be sheet.insertRule
. The function accepts two arguments:
rule
Rule is a string that specifies:
A selector and declaration
.pretty-text {
color: aquamarine;
}
Or an at-identifier and rule content
@media (max-width: 420px) {
.my-class { color: aquamarine; }
}
index
index
is a integer that defines the position to insert the rule
. Most of the time we are inserting our rule
at the end of the sheet.cssRules
list so it is common to see sheet.cssRules.length
used as the value of index
.
First Implementation
Let’s build a small abstraction for insertRule
so we can save ourselves some work.
function css(selector, styleString) {
const rule = `${selector} { ${styleString} }`;
const index = sheet.cssRules.length; // insert at the end sheet.insertRule(rule, index);
}
Lets try it out
Second Attempt
Nice. We have ourselves a decent utility function, but we can do better. The big problem is that we still have to pass in a CSS selector. This is more work than necessary. What if our utility function could generate unique selectors for us?
All we need is something unique about each rule to do this. Lucky for us, because we insert our rules at the end of our sheet, we have a constant unique value, `cssRules.length`
function css(styleString) {
const index = sheet.cssRules.length;
const id = index.toString(36);
const className = `css-${id}`;
const rule = `.${className} { ${styleString} }`;
sheet.insertRule(rule, index);
return className;
}
This is great! We have a function that takes styles in and returns a class name that we can use anywhere in the DOM.
Fun With Tagged Templates
There is one problem. I would rather write my styles like this.
const errorCls = css`
color: palevioletred;
font-size: 2.5rem;
margin: 0;
`
Doesn’t that look better?
Ok, if this is what we want we should go read Max Stoiber explain how template literals work in styled-components or Wes Bos’s great article on the subject.
First, we need to update the arguments of css
to those of a tagged template literal.
function css(strings, ...interpolations) {
const stringStyles = ???
// rest of css is unchanged
}
Next, we need a string from these arguments so we join, or interleave, the strings
and interpolations
arrays into a single string. We’ll loop over each string and append an interpolation if it exists at that index.
function interleave (strings, interpolations) {
return strings.reduce(
(final, str, i) =>
final +
str +
(interpolations[i] === undefined
? ""
: interpolations[i]),
""
);
}
Lets put it all together.
Pretty neat huh?
Closing Thoughts
Most CSS-in-JS libraries build on a foundation very similar to this. The biggest difference is that most existing libraries use some sort of style parser like Stylis.js or postcss-js that can handle features like nesting and media queries. A great exercise would be to add a parser to this example. If you do, please share!
More Reading:
DOM Enlightenment Chapter on CSS
Thanks to Kyle Shevlin and Peter Piekarczyk for the edits.