Build your own emotion like CSS-in-JS library.
Tags: css, javascript, cssinjs, webdev
Published: 2022-08-15 09:06:00

In this blog post, we will try to understand CSS-in-JS by creating our own version of [emotion.js](https://emotion.sh/docs/introduction). Emotion is one of the most popular CSS-in-JS solutions out there. 

There are some brilliant minds working on emotion, we aren't going to recreate emotion with all its complexities and optimization just that we are trying to develop a better understanding of such CSS-in-JS libraries by building one on our own.

Let us start by exploring API and see what emotion does for us.

![emotion.js css function](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qt9bsha39e3qc0c54bz2.png)


So the emotion package exports a function called `css` which takes `style-object` as an argument and returns a unique `className` which is used by our`div` to apply some style. 


![css function generates unique classname](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mnazyr9tkt5gwadhjopu.png)

Another overload signature `css` function can also take an array of `style-object` as an argument. For example: 

```jsx
css(
  [  // <-- array of 
    { padding: '32px', backgrounColor: 'orange' }, // <-- style object 1
    { fontSize: '24px', borderRadius: '4px' } // <-- style object 2
  ]
)
```

In the DOM `css` function injects a `<style>` in the `document.head` where it keeps the compiled CSS created from calling `css` function on the `style-object`.


![Injected styles](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7ihp0ok1cq4cud9rr8i3.png)

Summarising emotion `css` function:
- `css` function takes an object or array of objects as an argument.
- These objects are called `style-object` which is a way of writing CSS with JavaScript objects.
- It returns a unique `className` for the given `style-object`.
- It compiles the `style-object` into valid `CSS` and injects a `style` tag containing our compiled `CSS` into the `document.head`

## The `css` Function

Let's break it down is a series of programmatic steps:

1. Convert to a valid `style-object`.
2. Generate a unique className for the `style-object`.
3. Parse the `style-object` to generate valid CSS styles and attach them to className.
4. Inject the parsed CSS into a stylesheet in DOM.

Following our programmatic steps our `css` function should look like:

```js
function css(styles) {
  
  // 1. convert to valid style-object
  const _style_object_ = getValidStyleObject(styles);

  // 2. generate unique className
  const className = getClassName(_style_object_);

  // 3. Parse the style-object to generate valid CSS styles and attach them to className
  const CSS = parseStyles(_styles_object_, className);

  // 4. Create or update the stylesheet in DOM
  injectStyles(CSS);

  // return className to be applied on element
  return className;
}```

### Step 1: Convert to a valid  `style-object`

The `css` function can accept a `style-object` or an array of `style-object`. In the case of the array of `style-object` we must merge those to generate a single style object.

```js
function getValidStyleObject(styles) {
  let style_object = styles;
  
  if (Array.isArray(styles)) {
    style_object = merge(styles);
  }

  return style_object;
}

function merge(styles) {
  // (*) shallow merge
  return styles.reduce((acc, style) => Object.assign(acc, style), {}); 
}
```

It should be noted that this is a shallow merge which will simply replace the properties of the former one with a later one, for the nested properties simple replacement may cause an issue so we out for [deep-merge](https://thewebdev.info/2021/03/06/how-to-deep-merge-javascript-objects/) if required.

### Step 2: Generate a unique className for the `style-object`

After step 1 we have received a valid `style-object` and now we can process this object to generate unique className for it.
- Generating a unique className is required makes sure that there are no naming conflicts anywhere in the application; 
- Unique className eliminates the need for any naming conventions like [BEM](https://css-tricks.com/bem-101/), which makes the life of the dev easier. 
- For generating names we should make sure that we always come up with the same name for the same 
structured style object.

```js
const styleObject1 = {
  fontSize: '16px',
  fontWeight: 600
};

const styleObject2 = {
  fontSize: '16px',
  fontWeight: 600
};

styleObject1 === styleObject2; // false: reference is different
getClassName(styleObject) === getClassName(styleObject2); // true: Pure and Idempotent nature
```

- For maintaining Pure and Idempotent nature of `getClassName` function we will hash `style-object` so that it always returns the same output className for the same *structured* `style-object`. The hashing function needs input to be a string so we need to convert our `style-object` into a string. I will simply use `JSON.stringify` for our case. But there is a catch see below.

```js
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 2, a: 1 };

obj1 == obj2; // false: diff references

// an ideal stringifying function
stringify(obj1) === stringify(obj2) // true: '{ "a": 1, "b": 2 }'

// our JSON.stringify 
JSON.strinfigy(obj1) === JSON.stringify(obj2) // false
// '{ "a": 1, "b": 2 }' === '{ "b": 2, "a": 1 }' // false
``` 


- `JSON.stringify` is not an ideal stringifying utility as for same looking object it gives different string output. So If we plan to use `JSON.stringify` our hashes will also vary.

```js
// it is a cache map of "serialized-style-object" to "hashed-style-object"
const style_classname_cache = {};

function getClassName(styleObject) {
  const stringified = stringify(styleObject);

  // pick the cached className to optimize and skip hashing every time
  let className = style_classname_cache[stringified];
  
  // if there is not an entry for this stringified style means it is new
  // so generate a hashed className and register and entry of style

  if (!className) {

    // use any quick hashing algorithm
    // example: https://gist.github.com/jlevy/c246006675becc446360a798e2b2d781
    
    const hashed = hash(stringified);
    // prefix some string to indicate it is generated from lib
    // it also makes sure that className is valid
    const _class_name_ = `css-${hashed}`;
    
    // hashing is costly so make an entry for the generated className
    style_classname_cache[stringified] = _class_name_;
    
    className = style_classname_cache[stringified];
  }
  
  return className;
}
```
Now let's proceed to next step where we will parse the `style-object` to generate CSS string.

```js
// it is a map of "stringified-style-object" to "hashed-classname"
const style_classname_cache = {};

// inside css function
// ...
const className = getClassName(....);

let CSS = classname_css_cache[className];

if (!CSS) {
  CSS = parseStyles(_style_object_, className); // <-- Step 3
  classname_css_cache[classname] = CSS;
}
```
### Step 3: Parse the `style-object` to generate valid CSS styles and attach them to className

This is the toughest part where we process the `style-object` and generate valid CSS rule declations from them. Before we proceed let's take an example:

```js
  // style-object
  const styles = { 
    width: '600px',
    fontSize: '16px', // style-rule 1
    fontWeight: 600, // style-rule 2,
    color: 'red',
    '&:hover, &:active': {
        color: 'green',
    },
    '&[data-type="checkbox"]': {
      border: '1px solid black'
    },
    '@media(max-width: 1200px)': {
       width: '200px'
    }
   };

  css(styles)

  // compiled CSS from `style-object`
  .css-123 { 
    font-size: 16px;
    font-weight: 600px;
  }

  // ! NOTE !

  // 1) .css-123 is selector name or class-name here
  // 2) { and } marks style blocks/bound for this selector, each 
  //      block need to be parsed
  // 3) font-size: 16px is processed CSS for`fontSize: '16px'`
  // 4) `&:hover, &:active' are two blocks ideally joined by a `,`
  //      i.e '&:hover' and '&:active'
  // 5)  '&:hover' block is read as `css-123:hover` where
  //      `&` is replaced by current selector name
  // 6) `&[data-type="checkbox"]` attributes based styling is also possible
  // 7) @ rules are specific rule ex: @media screen size rules 
  //      so it should be processed early
  // 8) each nested style (ex: &:hover) need to be parsed 
  //      i.e recursive calling
```

From the above-gathered notes, we can write our `parseStyles` as

```js
function parseStyles(style_object, selector) {
  // This collects `@import` rules  which are independent of any selector
  let outer = "";

  // This is for block rules collected
  let blocks = "";
  2;

  // This is for the currently processed style-rule
  let current = "";

  // each property of style_object can be a rule (3)
  // or a nested styling 7, 8
  for (const key in style_object) {
    const value = style_object[key];

    // @ rules are specific and may be further nested
    // @media rules are essentially redefining styles on-screen breakpoints
    // so they need to be processed first
    const isAtRule = key[0] === "@";

    if (isAtRule) {
      // There are 4 main at-rules
      // 1. @import
      // 2. @font-face
      // 3. @keyframe
      // 4. @media

      const isImportRule = key[1] === "i";
      const isFontFaceRule = key[1] === "f";
      const isKeyframeRule = key[1] === "k";

      if (isImportRule) {
        // import is an outer rule declaration
        outer += key + " " + value; // @import nav.css
      } else if (isFontFaceRule) {
        // font face rules are global block rules but don't need a bound selector
        blocks += parseStyles(value, key);
      } else if (isKeyframeRule) {
        // keyframe rule are processed differently by our `css` function
        // which we should see implementation at a later point
        blocks += key + "{" + parseStyles(value, "") + "}";
      } else {
        // @media rules are essentially redefining CSS on breakpoints
        // they are nested rules and are bound to selector
        blocks += key + "{" + parseStyles(value, selector) + "}";
      }
    }
    // beside the At-Rules there are other nested rules
    // 4, 5, 6
    else if (typeof value === "object") {
      // the nested rule can be simple as "&:hover"
      // or a group of selectors like "&:hover, &:active" or
      // "&:hover .wrapper"
      // "&:hover [data-toggled]"
      // many such complex selector we will have to break them into simple selectors
      // "&:active, &:hover" should be simplified to "&:hover" and "&:active"
      // finally removing self-references (&) with class-name(root-binding `selector`)
      const selectors = selector
        ? // replace multiple selectors
          selector.replace(/([^,])+/g, (_seletr) => {
            // check the key for '&:hover' like

            return key.replace(/(^:.*)|([^,])+/g, (v) => {
              // replace self-references '&' with '_seletr'

              if (/&/.test(v)) return v.replace(/&/g, _seletr);

              return _seletr ? _seletr + " " + v : v;
            });
          })
        : key;
      // each of these nested selectors create their own blocks
      // &:hover {} has its own block
      blocks += parseStyles(value, selectors);
    }
    // now that we have dealt with object `value`
    // it means we are a simple style-rules (3)
    // style-rule values should not be undefined or null
    else if (value !== undefined) {
      // in JavaScript object keys are camelCased by default
      // i.e "textAlign" but it is not a valid CSS property
      // so we should convert it to valid CSS-property i.e "text-align"

      // Note: the key can be a CSS variable that starts from "--"
      // which need to remain as it is as they will be referred by value in code somewhere.
      const isVariable = key.startsWith("--")

      // prop value as per CSS "text-align" not "textAlign"
      const cssProp = isVariable
        ? key
        : key.replace(/[A-Z]/g, "-$&").toLowerCase();

      // css prop is written as "<prop>:<value>;"
      current += cssProp + ":" + value + ";";
    }
  }

  return (
    // outer are independent rules
    // and it is most likely to be the @import rule so it goes first
    outer +
    // if there are any current rules (style-rule)(3)
    // attach them to selector-block if any else attach them there
    (selector && current ? selector + "{" + current + "}" : current) +
    // all block-level CSS goes next
    blocks
  );
}
```

At this point, we have compiled CSS from `style_object` and all that is left is to inject it into the DOM.
### Step 4: Inject the parsed CSS into a stylesheet in DOM

For this step, we will create a `<style>` tag using `document.createElement` and inside of that style tag, we will append our styles in the`textNode`.

- Create a `<style id="css-in-js">` element if doesn't already exist;
- Get the text-node i.e `stylesheet.firstChild` and append CSS string from `parseStyles` in it.

```js
// in case the process isn't running in a browser instance 
// so we fake stylesheet-text-node behavior 
const fake_sheet = {
  data: ''
};

// keep track of all styles inserted so that we don't insert the same styles again
const inserted_styles_cache = {};

function injectStyles(css_string) {
  // create and get the style-tag; return the text node directly
  const stylesheet = getStyleSheet();

  // if already inserted style in the sheet we might ignore this call
  const hasInsertedInSheet = inserted_styles_cache[css_string];
  // these styles need to be inserted
  if (!hasInsertedInSheet) {
    stylesheet.data += css_string; // <-- inserted style in sheet
    inserted_styles_cache[css_string] = true; // <-- mark the insertion
  }
}

function  getStyleSheet() {
  // we aren't in the browser env so our fake_sheet will work
  if (typeof window === "undefined") {
      return fake_sheet;
  }
  
  const style = document.head.querySelector('#css-in-js');
  
  if (style) {
    return style.firstChild; // <-- text-node containing styles
  }
  
  // style doesn't already exist create a style-element
  const styleTag = document.createElement('style');

  styleTag.setAttribute('id', 'css-in-js');
  styleTag.innerHTML = ' ';

  document.head.appendChild(styleTag);

  return styleTag.firstChild; // <-- text-node containing styles
}

```

🎉*** Congratulations with that in place we have created our own CSS-in-JS library.*** 🎉

As for the `keyframes`, we can use our `css` function but with little modifications. 
Let's see the API and how its use first.

```js
const growAnimationName = keyframes({ // <-- argument is called keyframe-style-object
  from: { transform: 'scale(1)' }, 
  to: { transform: 'scale(2)' },
}); // <-- call to keyframe with style-object returns animation-name. eg: (css-987)

// used as
css({  width: '100px', height: '100px',  animation: `${growAnimationName} 2s ease infinite` });

// compiled as
//  @keyframe css-987 {  
//     from: { transform: scale(1) };
//     to: { transform: scale(2) };
//  }
```
- Keyframes have a similar API where it takes a `keyframe-style-object`.
- Keyframes return to the animation name; they are not bound to a class/selector scope.
- The `css` function only needs an animation name to apply styling which means keyframes need not be in `css` function style object definition.
- Keyframes are global where `keyframe-style-object` is stringified and hashed to generate animation name same as in the case generating className from any `style-object`. 
- These names are the only scope of keyframes it is global.
- If you note carefully we never write the `@keyframes` keyword in the `keyframes` function call so that is something added internally along with the animation-name.
- This conversion from a `keyframe-style-object` to `style-object` can look something like:

```js
// keyframe-style-object
{   
  from: { transform: 'scale(1)' }, 
  to: { transform: 'scale(2)' }
}

// converted style-object
{
  [`@keyframes ${animationName}`]: keyframe-style-object 
}```

Adding keyframes support to `css` function can be done simply by telling `css` function to treat this `css` call as a `keyframe` function call and do the above conversion before parsing the `style-object`.

```js
// adding one more parameter called `options` 
// this can be used to change the behavior of `css` function and
// it should be an optional parameter.
// changing the name to _css_ to indicate this is not exported and passing
// different values of options can yield different variations of _css_ functions
// to suit different requirements example keyframes

function _css_(styles, options) {
  // ...same no change...

  // in the parsing of the style function call

  parseStyles(
    // style-object
    options.hasKeyframes 
      ? 
        // convertion to valid style-object from a keyframe-style-object
        { [`@keyframe ${className}`]: _style_object_ }
     : 
        _style_object_,
    // selector
    className
  )
  
  // ...same no change...
}

// final exported function from library
export const css = (style_object) => _css_(style_object, {});
export const keyframes = (style_object) => _css_(style_object, { hasKeyframes: true });
```

With `keyframes` in place, we have successfully coded our CSS-in-JS library. So as promised we have created our emotion like library; Note that emotion is way more complex and handles many different edge cases with far better optimizations.

#### Summary of `css` function
- `css` function takes `style-object` or an array of `style-object`.
- It stringifies this `style-object` and generates a unique hashed representational string for it, eg:`css-123`.
- For the `keyframe` we convert `keyframe-style-object` to valid a `style-object` representation of `@keyframe` keyword.
- These styles are then parsed. Each property in `style-object` may be on the of the following `At(@) rules`, `&:hover`i.e multiple nested selector rules or `fontSize: '16px'` simple CSS properties. Each is dealt with differently as some can be block-scoped while others are global. Self-references using `&` are also handled here. After the correct parsing, we generate a valid `CSS` string representation of our `style-object`.
- This `CSS` string is added into a stylesheet in DOM and appended to `document.head`.

And now as for naming this library, I will like to call it - **Styler** 

- [Styler GitHub](https://github.com/vtechguys/styler) 
- [Styler Codesandbox](https://codesandbox.io/s/styler-sd2xbl)
- [Why CSS-in-JS?](https://aniketjha.dev/why-css-in-js)
- [CSS: Isolation vs Abstraction](https://aniketjha.dev/css-isolation-vs-abstraction)