What is Zero-Runtime CSS in JS? Which Library Should You Pick?
mk668a
Posted on October 29, 2023
Introduction: The End of CSS in JS and the Transition to Zero-Runtime
Front-end technology is changing rapidly.
In the midst of such change, React is leading the way among front-end frameworks, and in terms of CSS, CSS in JS is becoming mainstream.
On a side note, I used to worry about naming and designing CSS, using CSS Lint, and other aspects of efficient CSS management. However, I began to use component libraries like emotion for CSS in JS and MUI frequently, which made me stop thinking about CSS-related matters and made development easier. In recent projects, there are situations where there are no css or scss files present within the project.
However, believe it or not, CSS in JS is already being called outdated! I was curious about why it was being called outdated and found the following article:
From here, I'll explain the contents of the above article.
Why We're Breaking Up with CSS-in-JS
Sam Magura is known as the second most active maintainer of the CSS-in-JS library, Emotion. From his experience and knowledge, he points out certain issues with using CSS-in-JS. He initially was captivated by the benefits of CSS-in-JS, but it seems he began to feel its limitations through performance issues and other challenges faced while using it with the Spot team.
Drawbacks of CSS in JS
- Runtime Overhead: CSS-in-JS requires converting the style into plain CSS to insert it into the document when a component is rendered. This conversion process demands additional CPU cycles. This overhead can potentially impact the performance of the application.
- Increased Bundle Size: Since users visiting a site need to download the JavaScript of the CSS-in-JS library, their bundle size increases. For example, Emotion is 7.9 kB (minzipped) and styled-components are 12.7 kB. While these libraries aren't massive, they do add to the overall size.
- Cluttered React DevTools: Libraries like Emotion insert internal components into the React tree. This can make React DevTools cluttered, potentially complicating debugging.
- Additional Browser Work: Frequently inserting CSS rules places more workload on the browser. Especially in React's concurrent rendering mode, when a new rule is inserted, the browser has to verify whether or not that rule applies to the existing tree. This results in a style recalculation for all CSS rules and all DOM nodes while React is rendering, which can be extremely slow.
- Compatibility with SSR and Component Libraries: When using Emotion with server-side rendering or other component libraries that use Emotion, various issues can arise. These include multiple loads of the Emotion instance, not being able to fully control the order of style insertion, and differing SSR support for Emotion between React 17 and React 18.
The Future of CSS Libraries
As new styling approaches like CSS-in-JS emerge, traditional CSS libraries and frameworks continue to evolve. Preprocessors like Sass and Less provide features such as variables and mixins, making CSS writing more efficient.
Additionally, utility-first CSS frameworks like Tailwind CSS are gaining popularity. They allow rapid design construction by combining class names.
Considering the issues with CSS-in-JS, it's anticipated that traditional CSS libraries and the new utility-first approach will play a significant role in future web development.
Transition to Zero-Runtime CSS in JS
As mentioned in the previous article, there seems to be a problem with the occurrence of overhead. This overhead can decrease the overall page load and rendering speeds. This led to the introduction of zero-runtime CSS in JS.
What is Zero-Runtime CSS in JS?
Zero-runtime CSS in JS refers to the technique of writing CSS inside JavaScript and generating actual CSS files at build time. Compared to regular CSS in JS, its advantage is that it can reduce the execution time cost of JavaScript while retaining the convenience of writing CSS in JavaScript.
Benefits of Zero-Runtime CSS in JS
Zero-runtime CSS in JS is an approach that doesn't dynamically generate and inject CSS during runtime. All styles are generated at build time, which means there's no additional JavaScript overhead during runtime.
- Performance: The zero-runtime approach has the potential to improve page load and rendering speeds because there's no overhead from dynamically generating and injecting styles during runtime.
- Predictability: Since styles are generated at build time, there's a reduced risk of unexpected style changes or side effects during runtime.
- Smaller Bundle Size: Some zero-runtime libraries can reduce the final bundle size by eliminating unnecessary runtime code.
- Simplified Server-Side Rendering (SSR): Some CSS-in-JS solutions require additional configurations or steps during server-side rendering, but the zero-runtime approach can alleviate or eliminate this issue.
- Advantages of Static Analysis: With styles generated at build time, tools and linters can more easily perform static analysis of the styles.
- Environmental Compatibility: In some environments or frameworks, dynamically injecting styles during runtime can be challenging or not recommended. The zero-runtime approach works seamlessly in these situations.
Using CSS in JS with Next.js
The official Next.js documentation lists libraries that can be used within the App Router's app directory. As of the current date (2023/10/30), it appears that they only support usage within client components.
List of Zero-Runtime CSS-in-JS Libraries
After researching the existing zero-runtime CSS-in-JS, here are the libraries that are still actively developed:
- Linaria
- vanilla-extract
- Panda CSS
- Goober
- Astroturf
- Treat
So, Which CSS-in-JS Library Should You Use?
I plan to compare each of them from various perspectives to determine the best CSS-in-JS solution.
Library | Link | Main Features | Static CSS Generation | TypeScript Support | SSR Support | GitHub Stars | Development Period |
---|---|---|---|---|---|---|---|
Linaria | Linaria on GitHub | CSS is extracted into a CSS file at build time. Compatible with almost all modern frameworks. Can use dynamic styles based on React props (using CSS variables). Easily locate where styles are defined with CSS source maps. Lint CSS in JS with stylelint. Use any CSS pre-processor like Sass or PostCSS if needed. Supports atomic styles with @linaria/atomic. Compared to regular CSS, selectors are scoped, styles are in the same file as components, making refactoring easier. Interoperable with other CSS in JS libraries. Operates without JavaScript. | Extracts CSS at build time and outputs it as a static CSS file. | Offers TypeScript support, but the setup might be a bit intricate. | None | 10.9k | 2017/5/21 (Latest: 2023/10/3) |
vanilla-extract | vanilla-extract on GitHub | Write styles using locally scoped class names and CSS variables, and generate static CSS files at build time. A slight abstraction of standard CSS. Works with any frontend framework/no framework. Locally scoped CSS variables, @keyframes, and @font-face rules. A high-level theme system that supports multiple themes simultaneously without globals. Utilities for generating variable-based calc expressions. Type-safe styles via CSSType. Optional runtime version for development and testing. Optional API for dynamic runtime theming. | Generates static CSS at build time. | Provides type-safe styling with TypeScript. | None | 8.8k | 2021/2/21 (Latest: 2023/9/16) |
Panda CSS | Panda CSS on GitHub | Extracts style objects and style props at build time. Modern CSS output - cascading layers @layer, CSS variables, etc. Compatible with most JavaScript frameworks. Supports recipes and variants like stitches (stitches.dev). A high-level design token system that supports multiple themes simultaneously. Type-safe styles and autocomplete through code generation. Inspired by projects like Chakra UI, Vanilla Extract, Stitches, Tailwind CSS, Styled System, etc. | Generates static CSS at build time. | Provides type-safe styling with TypeScript. | Supports SSG and SSR. | 3.7k | 2022/1/24 (Latest: 2023/10/29) |
Goober | Goober on GitHub | Less than 1KB footprint. Developed with the intent of achieving the existing styled pattern with a smaller footprint. Supports the styled pattern similar to styled-components and emotion. Faster in SSR benchmarks compared to other major CSS-in-JS libraries. Goober is compatible with vanilla JavaScript, and frameworks like React, Vue.js, Angular, Svelte, etc. Integration with tools like Babel, Next.js, Gatsby, etc., is easy through plugins and macros. Supports features like shared styles, automatic prefixing, etc. Supports major browsers and older browsers can be supported using Babel. | Uses the extractCss function to extract static CSS at build time and injects it into the tag. Mainly used during server-side rendering. | Information about explicit TypeScript support is limited. | Supports SSR, provides extractCss function. | 3k | 2019/1/27 (Latest: 2023/4/19) |
Astroturf | Astroturf on GitHub | Write CSS inside JavaScript files, but operates without adding a runtime layer and works alongside the existing CSS processing pipeline. Avoids the loss of flexibility requiring framework-specific CSS handling and keeps CSS fully static without parsing runtime styles. Can write style definitions inside JavaScript files while using Sass, PostCSS, Less, etc. Enjoys the benefits of styling within JavaScript without runtime overhead while maintaining compatibility with existing CSS tools. Compatible with frameworks and supports React's props feature. | Generates static CSS without adding a runtime layer and works with the existing CSS processing pipeline. | Provides TypeScript support, but might require third-party libraries. | None | 2.2k | 2016/10/16 (Latest: 2023/5/17) |
Treat | Treat on GitHub | A framework-agnostic library. Optimizes bundle size with theme support and lightweight runtime style definition via static extraction. Supports webpack, React, and TypeScript. Supports legacy browsers. | Generates all CSS rules at build time and bundles only the generated CSS styles. | Provides type-safe styling with TypeScript. | None | 1.2k | 2019/5/12 (Latest: 2021/4/28) |
I am planning to select the best CSS in JS based on the following perspectives:
Library Maintenance
Treat hasn't been updated since 2021, suggesting there might be potential issues in its ongoing maintenance.
Static CSS Generation
While there are differences in the process of static CSS generation, every library produces static CSS at build time, reducing runtime overhead.
TypeScript Support
vanilla-extract, Panda CSS, and Treat all offer problem-free type-safe styling. For other libraries, there might be a need for additional libraries or more complicated configurations.
SSR Support
Panda CSS and Goober support SSR. For the other libraries, there's insufficient information to confirm if they definitely support SSR.
Performance
Based on various articles, there doesn't seem to be any difference in performance, like rendering time, among the libraries. As they're converted into static CSS, there doesn't seem to be much variance.
Style Writing
- Linaria
import { css } from '@linaria/core';
import { modularScale, hiDPI } from 'polished';
import fonts from './fonts';
// Write your styles in `css` tag
const header = css`
text-transform: uppercase;
font-family: ${fonts.heading};
font-size: ${modularScale(2)};
${hiDPI(1.5)} {
font-size: ${modularScale(2.5)};
}
`;
// Then use it as a class name
<h1 className={header}>Hello world</h1>;
import { styled } from '@linaria/react';
import { families, sizes } from './fonts';
// Write your styles in `styled` tag
const Title = styled.h1`
font-family: ${families.serif};
`;
const Container = styled.div`
font-size: ${sizes.medium}px;
color: ${props => props.color};
border: 1px solid red;
&:hover {
border-color: blue;
}
${Title} {
margin-bottom: 24px;
}
`;
// Then use the resulting component
<Container color="#333">
<Title>Hello world</Title>
</Container>;
- vanilla-extract
// styles.css.ts
import { createTheme, style } from '@vanilla-extract/css';
export const [themeClass, vars] = createTheme({
color: {
brand: 'blue'
},
font: {
body: 'arial'
}
});
export const exampleStyle = style({
backgroundColor: vars.color.brand,
fontFamily: vars.font.body,
color: 'white',
padding: 10
});
// app.ts
import { themeClass, exampleStyle } from './styles.css.ts';
document.write(`
<section class="${themeClass}">
<h1 class="${exampleStyle}">Hello world!</h1>
</section>
`);
- Panda CSS
import { css } from '../styled-system/css'
import { stack, vstack, hstack } from '../styled-system/patterns'
function Example() {
return (
<div>
<div className={hstack({ gap: '30px', color: 'pink.300' })}>Box 1</div>
<div className={css({ fontSize: 'lg', color: 'red.400' })}>Box 2</div>
</div>
)
}
import { css } from '../styled-system/css'
import { styled } from '../styled-system/jsx'
// The className approach
const Button = ({ children }) => (
<button
className={css({
bg: 'blue.500',
color: 'white',
py: '2',
px: '4',
rounded: 'md'
})}
>
{children}
</button>
)
// The style props approach
const Button = ({ children }) => (
<styled.button bg="blue.500" color="white" py="2" px="4" rounded="md">
{children}
</styled.button>
)
- Goober
import { h } from 'preact';
import { styled, setup } from 'goober';
// Should be called here, and just once
setup(h);
const Icon = styled('span')`
display: flex;
flex: 1;
color: red;
`;
const Button = styled('button')`
background: dodgerblue;
color: white;
border: ${Math.random()}px solid white;
&:focus,
&:hover {
padding: 1em;
}
.otherClass {
margin: 0;
}
${Icon} {
color: black;
}
`;
interface Props {
size: number;
}
styled('div')<Props>`
border-radius: ${(props) => props.size}px;
`;
// This also works!
styled<Props>('div')`
border-radius: ${(props) => props.size}px;
`;
- Astroturf
import * as React from 'react';
import { css } from 'astroturf';
function Button({ children, ...props }) {
return (
<button
{...props}
css={css`
color: blue;
border: 1px solid blue;
padding: 0 1rem;
`}
>
{children}
</button>
);
}
- Treat
// Button.treat.js
// ** THIS CODE WON'T END UP IN YOUR BUNDLE EITHER! **
import { style } from 'treat';
export const button = style((theme) => ({
backgroundColor: theme.brandColor,
height: theme.grid * 11
}));
// Button.js
import React from 'react';
import { useStyles } from 'react-treat';
import * as styleRefs from './Button.treat.js';
export const Button = (props) => {
const styles = useStyles(styleRefs);
return <button {...props} className={styles.button} />;
};
Selection: Panda CSS
After considering various perspectives, I believe that Panda CSS is the overall best choice. However, its style of writing is unique, so if you prefer the writing style of libraries like styled-components or emotion, then Goober might be more to your liking. Additionally, Panda CSS has been developed drawing inspiration from projects like Chakra UI, Vanilla Extract, Stitches, Tailwind CSS, and Styled System. Given that it's a relatively new library, it seems to have effectively incorporated the best features from established libraries.
(Supplementary Information) Zero-runtime for Headless Components is Emerging
The following Kuma UI provides a hybrid approach with headless components where styles can be described in props and className, making it easy to implement.
Library | Key Features | GitHub Stars | Link |
---|---|---|---|
Kuma UI | Achieves a seamless development experience with automatic style completion. All components within the library are style-free, offering the utmost flexibility for users to apply their own styles. Supports any description style with a hybrid approach. Constantly up-to-date with cutting-edge Next.js technology through RSC support. Familiar API design. Extracts styles that can be determined at build time statically, and adopts a method to inject them at runtime by performing a static "dirty check" for styles that might change dynamically. Incorporates the best features inspired by libraries such as Styled System, Chakra UI, Native Base, Panda CSS, Linaria, Vanilla Extract, and more. | 1.3k | Kuma UI GitHub |
Style Writing
- Kuma UI
function App() {
return (
<Box as="main" display="flex" flexDir={["column", "row"]}>
<Heading
as="h3"
className={css`
color: red;
@media (max-width: sm) {
color: blue;
}
`}
>
Kuma UI
</Heading>
<Spacer size={4} />
<Flex flexDir={`column`}>
<Text as="p" fontSize={24}>
Headless UI Component Library
</Text>
<Button variant='primary'>Getting Started</Button>
</Flex>
</Box>
);
}
Conclusion
In this article, we explored various zero-runtime CSS in JS libraries and tried to determine the best one. Depending on your priorities, such as emphasizing lightweight performance with Goober, your library choice may differ. It's essential to choose a library that suits your project based on each library's unique features. Thank you for reading this far.
Posted on October 29, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
June 3, 2023