https://unsplash.com/photos/3shfnfzdFVc

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

emotion’s css function

glamor’s css function

cxs’s styled function

Thanks to Kyle Shevlin and Peter Piekarczyk for the edits.