A Super Tiny Framework-Agnostic CSS-in-JS
Rajasegar Chandran
Hello everyone, My name is Rajasegar Chandran. I am from Chennai, India. It’s really been an honour and privilege for me to be part of Javascript and Friends, thank you for having me here.
About me
Front-end @ Freshworks Inc
This is about me. I am working as a Front-end Developer at a company called Freshworks. We build simple and innovative SAAS products which offer CRM solutions for businesses of all sizes.
CSS-in-JS
csz
This is a talk about a CSS-in-JS library called csz. Before diving into the topic let’s set some context about CSS-in-JS solutions, how it all started, why it became a viable option for styling components in the first place.
CSS: Problems at Scale
Global Namespace
Dependencies
Dead Code Elimination
Minification
Sharing Constants
Non-deterministic resolution
Isolation
Blog post
He talked about problems of using CSS at scale such as Global Namespace, Dependencies, Dead Code elimination and so on. He also showed us how we can use CSS in JS to solve these problems without bringing in any kind of hacky solutions.
Styled Components
Emotion
Glamorous
Fela
aphrodite
glamor
styletron
JSS
linaria
react-jss
From then on, the web development community has come up with a lot of CSS-in-JS solutions
CSS-in-JS
“ the overall ecosystem is still very fragmented and in
constant movement. So it remains to be seen what
place CSS-in-JS will occupy in the overall ecosystem in
the long run.”
CSS 2019
According to the State of CSS 2019, some libraries like Styled Components and Emotion have established themselves as solid options, the overall ecosystem is still very fragmented and in constant movement. So it remains to be a big question what place CSS-in-JS will occupy in the long run.
Compilation
Build
Static
Plugins
Syntax
Loaders
And using CSS-in-JS comes with its own set of challenges. We need to compile them because they use a special syntax which is not supported and again we need to load the necessary plugins and a build step is essential to generate the final css output.
Why?
Framework Agnostic CSS-in-JS
Load styles dynamically from .css files withouth FOUC
Efficient Caching of styles
Write CSS in SASS like Way
No Bundling / Compilation
You might be thinking why should I choose csz over other solutions, what kind of problems it solves for us?
Let’s look at a list of scenarios where we can make use of csz. If you are looking for a framework-agnostic CSS-in-JS solution, then csz can be the right candidate for you. If you want to load styles dynamically from .css files without having to worry about flashes of unstyled content then you should go for csz. If you want your style definitions to be cached effectively then csz might be the right choice for you. If you want to write CSS in SASS like style with nested selectors and all, then csz might be able to help you out. And finally if you are the one who is looking for a bundle-less solution for your css assets then csz is the perfect tool for the job.
Runtime CSS Modules with SASS like pre-processing
github.com/lukejacksonn/csz
It’s a runtime CSS Modules with SASS like preprocessing. It is created by Luke Jackson. Luke is a front-end developer from London. He also created some other amazing tools like perflink, servor, etc.,
Super Tiny
Just 49 lines of JavaScript
csz is super tiny in the sense that it’s just 49 lines of JS. What I mean by 49 lines is the physical lines including all the line breaks.
Source code
import stylis from './stylis.js';
const cache = {};
const hash = () => Math.random() .toString(36) .replace('0.', '');
const sheet = document.createElement('style');
document.head.appendChild(sheet);
const none = hash => `.${hash}{display:none}`;
const hide = hash => (sheet.innerHTML = none(hash) + sheet.innerHTML);
const show =
hash => (sheet.innerHTML = sheet.innerHTML.replace(none(hash), ''));
const isExternalStyleSheet = key => /^(\/|https?:\/\/)/.test(key.trim());
const process = key => hash => rules => {
sheet.innerHTML += (cache[key] = {
hash,
rules: stylis()(`.${hash}`, rules)
}).rules;
if (isExternalStyleSheet(key)) show(hash);
};
export default (strings, ...values) => {
const key = strings[0].startsWith('/')
? strings[0]
: strings.reduce(
(acc, string, i) =>
(acc += string + (values[i] == null ? '' : values[i])),
''
);
if (cache[key]) return cache[key].hash;
const className = 'csz-' + hash();
const append = process(key)(className);
if (isExternalStyleSheet(key)) {
hide(className);
fetch(key)
.then(res => res.text())
.then(append);
} else append(key);
return className;
};
And if you remove the line breaks it’s just 31 lines and it can fit entirely in a single slide.
BundlePhobia
csz
And there is this thing called BundlePhobia.com where you can check the production footprint of the libraries you are using. And csz is just 12.3 kb when it is minified and 5 Kb when compressed with gzip. It has got no external dependencies.
Framework-Agnostic
React, Vue, Ember, Preact, Svelte, ...
And it is completely framework agnostic. It is not specifically tailored for any Javascript frameworks to work with. Unlike Styled Components which is mainly targeted for component based frameworks like React.js. Whereas csz can work with any framework. You can use it in frameworks like React, Ember, Vue.js, Preact and Svelte. It just works seamlessly.
Run-time only class name generation &
Ruleset Isolation
Luke was inspired by emotion and styled-components but unfortunately neither of these packages expose an ES module compatible build and come with quite a lot of extraneous functionality that isn't required when the scope of the project is restricted to runtime only class name generation and ruleset isolation.
Loading in stylesheets (.css) dynamically
during runtime in the browser (not compile time)
Loading in stylesheets dynamically – from .css files – is supported out of the box, so you can write your styles in .css files and import them via url without having to worry about flashes of unstyled content.
SASS like preprocessing
Nested Selectors
Global style injection
Vendor prefixing
csz supports SASS like preprocessing which means you can use nested selectors, or simply you can write your CSS in SASS like style. This would definitely help people who want to move from SASS to get rid of all the compilation and build stuff but still want to keep the good parts of SASS conventions. How does csz do that?
Csz actually relies on something called Stylis.
I am a center aligned text.
↓
It uses stylis to parse styles from tagged template literals and append them to the head of the document at runtime.
stylis
Sultan Tarimo
github.com/thysultan/stylis.js
Stylis is a light-weight CSS preprocessor created by Sultan Tarimo. Stylis is the preprocessor which is internally used by libraries like Styled-components and Emotion.
Stylis
Features:
Nesting a { &:hover {} }
Selector namespacing
Vendor prefixing (flex-box, etc...)
Minification
ESM module compatible
Tree-shaking-able
Stylis supports a wide array of features like Nesting, Selector namespacing, vendor prefixing and so on.
No build tools required
Just works in the browser on the fly...
And finally if you want to use csz, you don’t require any build tools or bundlers like Webpack. This is one of the distinguishing features of csz when compared with other CSS-in-JS libraries. csz does not need any compilation or build process. It is specifically designed to work directly in the browser.
Using csz
Now we will see how to use csz inside your applications. We will take a look at different use-cases, patterns and examples.
Importing csz (Node)
import css from 'csz';
if you are using any build tools like Webpack or any other bundlers, you import csz directly from the package as an ES6 Import.
Importing csz (Browser)
import css from 'https://unpkg.com/csz';
And you can also hotlink the package directly from unpkg.com.
unpkg
unpkg.com/:package@:version/:file
unpkg.com/react@16.7.0/umd/react.production.min.js
🛫
unpkg is a fast, global CDN for everything on npm. You can use it to quickly and easily load any file from any package using a URL. It was created by Michael Jackson. Did you know he hacked something together in an airport one night as he was waiting to board the plane and published it as npmcdn.com. About a year later he changed the name to unpkg.com and it stuck.
Inline usage
// static
// generate class name for ruleset
const inlined = css`background: blue;`;
If a ruleset is provided as a string then it is processed immediately
Using external stylesheets
// dynamic (from stylesheet)
// generate class name for file contents
const relative = css`/index.css`;
// dynamic (from url)
// generate class name for file contents
const absolute = css`https://example.com/index.css`;
but if a filepath is provided then processing is deferred until the contents of the file has been fetched and parsed.
Hello world example
This is a typical hello world example for csz. Just like the same example you might have seen in the Styled components documentation. Here we are using the inline style by applying the ruleset from tagged template literals to the class names of the elements.
Dynamically generated class names
.csz-pr8icladqd9 {
font-size:1.5em;
text-align:center;
color:palevioletred;
}
.csz-1vhdvy1k9f8 {
padding:4em;
background:papayawhip;
}
Output
Hello world example - variation 2
This is another variation, a more standard and common pattern of using style objects. Instead of applying inline we create an object for the parent element and apply that to the className and the style definitions and classes are automatically applied.
Adapting to props
Output
This is another example of using csz for adapting styles based on properties to a component. If the primary property is passed to this button component here, the background color and color property will be changed accordingly.
Pseudo selectors
Output
This is another example of using pseudo elements, pseudo selectors and sibling selectors,
Animations
Output
csz also supports Keyframe and animation namespacing
Theming - component definition
Output
This is an example of how you can implement themes using csz. This is a simple button component with theming provisions for the color and border. By default both of them will be having palevioletred color, if a theme prop is passed to the button, it will take the colors from the theme for styling the button.
Theming - component usage
Output
How?
Now we will take a look at the internals of csz. We will see how it works, what are the different moving parts involved. Since it is a simple library, we can visualise them in just 8 simple steps.
1. Import stylis
import stylis from './stylis.js';
csz
So the first stage is importing stylis. csz actually keeps a local copy of minified stylis.js within its repository to make sure it does not pull in any kind of npm dependencies.
2. Creating the hash
const hash = () =>
Math.random()
.toString(36)
.replace('0.', '');
0.9443615549023372
↓
"0.uzmb2hdq59f"
↓
"uzmb2hdq59f"
The second step is to create the hash. csz is using the random number generator from the standard Math library in JS and then converting it to a base 36 string representation and finally removing the whole number and the decimal point by just keeping the fractional part for the hash.
Base 36
hexatridecimal
A small trivia for the audience. Do you know what is base 36 number system is known as? You know there are binary, octal number systems, so similarly base 36 is called hexatridecimal. You know I found a new way to annoy people, so when someone ask me how old are you next time, I am going to say, Hey I am hexatridecimal.
3. Adding Internal Stylesheet
const sheet = document.createElement('style');
document.head.appendChild(sheet);
The 3rd step is to create a new internal stylesheet with the style tag and append it to the head of the document.
4. Temporary ruleset to prevent FOUC
const none = hash => `.${hash}{display:none}`;
const hide = hash => (sheet.innerHTML = none(hash) + sheet.innerHTML);
const show = hash => (sheet.innerHTML = sheet.innerHTML.replace(none(hash), ''));
Styles imported from a file are inevitably going to take some time to download. Whilst the stylesheet is being downloaded, a temporary ruleset is applied to the element, which hides it (using display: none) until the fetched files have been processed. This was implemented to prevent flashes of unstyled content.
Flash of Unstyled Content
Have you ever noticed an annoying "flash of unstyled content" (FOUC) when a web page first loads? That happens because browsers render things as quickly as possible, often BEFORE your JavaScript executes the first time. So what if some of your initial styles are set via JavaScript…
5. Caching
Inline Styles
External Stylesheets (both local and remote)
When it comes to caching styles, csz is having one of the best caching mechanisms. It enables csz to cache the inline style definitions as well as the external stylesheets fetched from both local and remote.
Cache Key
const key = strings[0].startsWith('/')
? strings[0]
: strings.reduce(
(acc, string, i) =>
(acc += string + (values[i] == null ? '' : values[i])),
''
);
This is how the cache keys are created.
Cache {}
Code
Cache Key
css`text-align:center;`
text-align:center;
css`/index.css`
/index.css
css`https://unpkg.com/tailwind.css`
https://unpkg.com/tailwind.css
if (cache[key]) return cache[key].hash;
Basically the keys are just the style definitions if it is an inline usage or the file name or url if it is an external stylesheet.
Code
Cache Key
css`text-align:center;`
text-align:center;
Let’s see an example of class name caching, in this example we have two p tags with classnames having the same ruleset, Since the cache key here is the same csz just creates a single class definition instead of two and apply the same class to both the p tags.
Output
6. Create class name & Process
const className = 'csz-' + hash();
const append = process(key)(className);
Next we are prepending the class names with csz- and then using the keys and classnames to process.
const process = key => hash => rules => {
sheet.innerHTML += (cache[key] = {
hash,
rules: stylis()(`.${hash}`, rules)
}).rules;
if (isExternalStyleSheet(key)) show(hash);
};
This is where all the processing takes place. This is where stylis comes into play to compile the styles and generate the css output.
External stylesheets
// Utility to check external stylesheet
const isExternalStyleSheet = key => /^(\/|https?:\/\/)/.test(key.trim());
The isExternalStyleSheet is just a utility function to check whether the given template literal is just a style definition or an url, either local stylesheet or remote.
Fetch & show stylesheets
if (isExternalStyleSheet(key)) {
hide(className);
fetch(key)
.then(res => res.text())
.then(append);
} else append(key);
Then based on the type of stylesheet it will append the style definitions. If it is an external stylesheet, it will fetch the resource and append, otherwise just append the styles to the document head.
import stylis from './stylis.js';
const cache = {};
const hash = () => Math.random() .toString(36) .replace('0.', '');
const sheet = document.createElement('style');
document.head.appendChild(sheet);
const none = hash => `.${hash}{display:none}`;
const hide = hash => (sheet.innerHTML = none(hash) + sheet.innerHTML);
const show =
hash => (sheet.innerHTML = sheet.innerHTML.replace(none(hash), ''));
const isExternalStyleSheet = key => /^(\/|https?:\/\/)/.test(key.trim());
const process = key => hash => rules => {
sheet.innerHTML += (cache[key] = {
hash,
rules: stylis()(`.${hash}`, rules)
}).rules;
if (isExternalStyleSheet(key)) show(hash);
};
export default (strings, ...values) => {
const key = strings[0].startsWith('/')
? strings[0]
: strings.reduce(
(acc, string, i) =>
(acc += string + (values[i] == null ? '' : values[i])),
''
);
if (cache[key]) return cache[key].hash;
const className = 'csz-' + hash();
const append = process(key)(className);
if (isExternalStyleSheet(key)) {
hide(className);
fetch(key)
.then(res => res.text())
.then(append);
} else append(key);
return className;
};
And finally it just returns the class name that was dynamically generated using the hash. That’s all in csz, as you can see it’s just a simple library with wonderful capabilities.