Sign Up

Sign Up to our social questions and Answers Engine to ask questions, answer people’s questions, and connect with other people.

Have an account? Sign In

Have an account? Sign In Now

Sign In

Login to our social questions & Answers Engine to ask questions answer people’s questions & connect with other people.

Sign Up Here

Forgot Password?

Don't have account, Sign Up Here

Forgot Password

Lost your password? Please enter your email address. You will receive a link and will create a new password via email.

Have an account? Sign In Now

Sorry, you do not have permission to ask a question, You must login to ask a question.

Forgot Password?

Need An Account, Sign Up Here

Please type your username.

Please type your E-Mail.

Please choose an appropriate title for the post.

Please choose the appropriate section so your post can be easily searched.

Please choose suitable Keywords Ex: post, video.

Browse

Need An Account, Sign Up Here

Please briefly explain why you feel this question should be reported.

Please briefly explain why you feel this answer should be reported.

Please briefly explain why you feel this user should be reported.

Sign InSign Up

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Logo Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Logo

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Navigation

  • Home
  • About Us
  • Contact Us
Search
Ask A Question

Mobile menu

Close
Ask a Question
  • Home
  • About Us
  • Contact Us
Home/ Questions/Q 6880

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Latest Questions

Author
  • 60k
Author
Asked: November 27, 20242024-11-27T10:39:09+00:00 2024-11-27T10:39:09+00:00

Build your own emotion like CSS-in-JS library.

  • 60k

In this blog post, we will try to understand CSS-in-JS by creating our own version of emotion.js. 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

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 ourdiv to apply some style.

css function generates unique classname

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

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

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

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:

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), {});  } 
Enter fullscreen mode Exit fullscreen mode

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 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, 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.
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 
Enter fullscreen mode Exit fullscreen mode

  • 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.
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 
Enter fullscreen mode Exit fullscreen mode

  • 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.
// 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; } 
Enter fullscreen mode Exit fullscreen mode

Now let's proceed to next step where we will parse the style-object to generate CSS string.

// 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; } 
Enter fullscreen mode Exit fullscreen mode

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:

  // 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 
Enter fullscreen mode Exit fullscreen mode

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

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   ); } 
Enter fullscreen mode Exit fullscreen mode

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 thetextNode.

  • 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.
// 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 }  
Enter fullscreen mode Exit fullscreen mode

🎉*** 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.

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) }; //  } 
Enter fullscreen mode Exit fullscreen mode

  • 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:
// keyframe-style-object {      from: { transform: 'scale(1)' },    to: { transform: 'scale(2)' } }  // converted 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 }); 
Enter fullscreen mode Exit fullscreen mode

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, &:hoveri.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
  • Styler Codesandbox
  • Why CSS-in-JS?
  • CSS: Isolation vs Abstraction

csscssinjsjavascriptwebdev
  • 0 0 Answers
  • 0 Views
  • 0 Followers
  • 0
Share
  • Facebook
  • Report

Leave an answer
Cancel reply

You must login to add an answer.

Forgot Password?

Need An Account, Sign Up Here

Sidebar

Ask A Question

Stats

  • Questions 4k
  • Answers 0
  • Best Answers 0
  • Users 2k
  • Popular
  • Answers
  • Author

    ES6 - A beginners guide - Template Literals

    • 0 Answers
  • Author

    Understanding Higher Order Functions in JavaScript.

    • 0 Answers
  • Author

    Build a custom video chat app with Daily and Vue.js

    • 0 Answers

Top Members

Samantha Carter

Samantha Carter

  • 0 Questions
  • 20 Points
Begginer
Ella Lewis

Ella Lewis

  • 0 Questions
  • 20 Points
Begginer
Isaac Anderson

Isaac Anderson

  • 0 Questions
  • 20 Points
Begginer

Explore

  • Home
  • Add group
  • Groups page
  • Communities
  • Questions
    • New Questions
    • Trending Questions
    • Must read Questions
    • Hot Questions
  • Polls
  • Tags
  • Badges
  • Users
  • Help

Footer

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise

Querify Question Shop: Explore, ask, and connect. Join our vibrant Q&A community today!

About Us

  • About Us
  • Contact Us
  • All Users

Legal Stuff

  • Terms of Use
  • Privacy Policy
  • Cookie Policy

Help

  • Knowledge Base
  • Support

Follow

© 2022 Querify Question. All Rights Reserved

Insert/edit link

Enter the destination URL

Or link to existing content

    No search term specified. Showing recent items. Search or use up and down arrow keys to select an item.