I've spent years working on complex web development projects, and saw the landscape of frontend development evolve rapidly, particularly in how we handle styling. For a long period of time, like many other devs, I used css-in-js in all sorts of libraries, but never really understood how they work under the hood.
In recent years we even heard of concepts like runtime / zero runtime css-in-js. This article has 2 parts - first one is an introduction to css-in-js concepts (particularly styled function), and how they work under the hood in both runtime and zero-runtime implementations. The second part will focus on jux toolkit, our own implementation for styled function.
The Evolution of Web Styling
When I started in web development, we had a clear separation:
- HTML for structure
- CSS for styling
- JavaScript for behavior
However, as applications became more complex, this separation began to blur. I remember reading a fascinating article by a prominent frontend developer who argued that most programmers excel at only two of the three core web technologies. This observation rings true in my experience. For lots of developers, css is the most frustrating part of their work. The amount of times I heard my colleagues say “why does this css doesn’t work!” is overwhelming.
The challenge we face today is that many developers, especially those coming from a pure programming background, find CSS unintuitive and prefer JavaScript. Naming classnames (and attaching them to relevant components), making a clear separation between css and components (who hasn’t changed a class just to find out it affected 10 other places in the UI ?), using css linters and variables management are only a partial list of those reasons.
This preference has led to the rise of CSS-in-JS solutions, with styled function being a prime example. This made us stop thinking about many css related issues and improved our developer experience.
This deep dive into css-in-js internals emerged from the research phase I did before developing jux toolkit, our zero-runtime css-in-js library. Mapping the pros and cons of each approach in the ecosystem (including the developer experience around using a library) was a challenging task, but with jux toolkit, we're trying to provide a cohesive solution that balances performance with developer experience. I encourage everyone to check it out, and feel free to reach out with any questions or feedback!
Understanding styled functions
Styled functions are part of a broader concept called CSS-in-JS. The idea is simple yet powerful: write your styles using JavaScript syntax, and let a library translate that into actual CSS that the browser can understand. This approach allows developers to leverage JavaScript's full power when writing styles, including variables, functions, and more complex logic.
This has several advantages:
- You can reference JS variables in style rules.
- Locally scoped styles - When writing plain css / scss and naming your own class names, it’s very easy to apply styles more widely than originally intended.
- Project structure / colocation - it’s very good practice to include everything related to a component in the same folder / file (css files are a prime example). Integrating styles into javascript components makes this task seamless.
- Ease of maintenance - Tight coupling of styles and components makes life much easier.
Let’s break down how a styled function works:
// 1. You write this
const Button = styled('button')({
backgroundColor: 'blue',
color: 'white',
padding: '8px 16px'
});
// 2. When used in a component
<Button>Click me</Button>
// 3. What gets rendered in your DOM
<button className="css-ad3fa2">Click me</button>
// 4. The CSS that gets generated
.css-ad3fa2 {
background-color: blue;
color: white;
padding: 8px 16px;
}
Pretty basic and simple. But it gets interesting while we cover what’s under the hood - Not all styled function implementations are created equal. There are two main approaches for implementing styled function: Runtime, and Zero-Runtime.
Runtime libraries
These libraries like Emotion and Styled Components compute CSS on the fly while the application runs in the client's browser. This approach offers extensive flexibility but comes with a performance cost, especially for complex applications.
In runtime CSS-in-JS libraries, the styled function works dynamically during the application's execution. Here's a (very) simplified implementation of a typical styled function:
// Simple cache to track injected classNames
const styleCache = new Set();
function styled(Component) {
return (styles) => {
// Return a new React component
return function StyledComponent(props) {
// Generate a consistent className based on styles hash
const className = generateHashedClassName(styles);
// Only inject styles if this className hasn't been injected before
if (!styleCache.has(className)) {
// Convert styles object to CSS string
// => '.hashedClassName { color: red; font-size: 12px; }'
const cssString = convertToCSSString(className, styles);
// Inject styles into the document head
injectStyles(cssString);
styleCache.add(className);
}
// Return the component with the generated className
return React.createElement(Component, {
...props,
className
})
};
};
}
function injectStyles(cssString) {
// Get our stylesheet if it exists, or create it
const styleElement = document.querySelector('style[data-styled]') || createStyleElement();
const sheet = styleElement.sheet;
sheet.insertRule(cssString, sheet.cssRules.length);
}
function createStyleElement() {
const style = document.createElement('style');
style.setAttribute('data-styled', '');
document.head.appendChild(style);
return style;
}
When we run const Button = styled('button')({...})
, a few things happen. Let's break it down:
- Button variable will be assigned
StyledComponent(props)
function - Once we render Button (
<Button … />
), StyledComponent function gets called. Inside this function we calculate the hash of the given styles, to generate a unique classname (e.g.css-7d3vh2
). - We check if the hash is already in the cache and was injected to the page. If not:
- We convert our styles object to a css string
- We inject the styles into the page using CSSOM
- We save the result into the cache.
- We apply the classname to our Component.
One aspect of styled functions is how they handle style reuse and optimization. Let's say you have an avatar component that you use 200 times in a table. A naive implementation might create 200 identical class definitions. However, a well-implemented styled function library will automatically cache and reuse styles, significantly reducing CSS generation.
Runtime implementation: injecting styles
A crucial detail of runtime css-in-js is that styles aren't injected when components are defined, but when they're first rendered to the page. Let's see this in action:
// This only creates the component definition
// No styles are injected yet!
const Button = styled('button')({
backgroundColor: 'blue',
color: 'white',
padding: '8px 16px'
});
const Card = styled('div')({
border: '1px solid #ccc',
borderRadius: '4px',
padding: '16px'
});
// Even with multiple styled components defined,
// no styles have been injected at this point
function App() {
const [showCard, setShowCard] = useState(false);
// Button styles will be injected on initial render
// Card styles will only be injected when showCard becomes true
return (
<div>
<Button onClick={() => setShowCard(true)}>
Show Card
</Button>
{showCard && <Card>Hello!</Card>}
</div>
);
}
This lazy injection pattern is particularly beneficial in larger applications and has several benefits:
- Initial page load only includes styles for visible components
- Conditional components' styles are deferred until needed
- Unused components never have their styles injected
- Better memory usage and performance in large applications.
This, combined with the caching we discussed earlier, means that even with many component instances (like our 200 avatars example), each unique style rule is only injected once, and only when it's actually needed on the page.
But one additional, significant advantage of runtime css-in-js is the ability to interpolate styles based on runtime values / dynamic component props (which is also now possible on some zero-runtime implementations. More on that later) Consider the following code:
function ColorPicker() {
const [color, setColor] = useState('#3498db');
const Box = styled('div')((props) => ({
width: '100px',
height: '100px',
backgroundColor: props.color, // Dynamic color from user input
borderRadius: '4px',
transition: 'background-color 0.2s',
border: '1px solid #ccc'
}));
return (
<div>
<input
type="color"
value={color}
onChange={(e) => setColor(e.target.value)}
/>
<Box color={color} />
</div>
);
}
What makes runtime css-in-js better than zero-runtime in that sense, is its ability to generate unique style rules for different, dynamic prop combinations. Every time the user picks a new color, our Box component receives a new color
prop, generating a new unique className like css-bw4k3p
. This is impossible to achieve with zero-runtime solutions because the color values can't be known until the user interacts with the page.
Note - We do have to be careful with this approach - Each unique color creates new style rule and a classname. In real applications, generating new styles for every possible color value (or any runtime value based on your use case) could lead to style bloat. For cases like this, you might want to consider using CSS custom properties (variables) or inline styles for the color property specifically.
Compositions
One of the great features of styled function is the ability to compose components with each other, essentially “extending” the styles of a given component.
const Button = styled('button')({
backgroundColor: 'transparent',
borderRadius: '8px',
padding: '8px 16px',
border: '1px solid',
},
variants: [
{
props: { type: 'primary' },
style: {
color: 'violet',
},
},
]});
const DangerButton = styled(Button)({
variants: [
{
props: { type: 'danger' },
style: { color: '#d13333' },
},
],
});
<Button type={'primary'}>I'm primary</Button>
<DangerButton type={'danger'}>I'm danger</DangerButton>
<DangerButton type={'primary'}>I'm danger but primary</DangerButton>
When you extend a component (DangerButton extending Button in this case), the styled function handles props delegation to the inner component. Let’s slightly modify our mini styled function to support style extension:
function styled(Component) {
return (styles) => {
// Return a new React component
return function StyledComponent(props) {
// Generate a unique className based on styles hash
const uniqueClassName = generateHashedClassName(styles);
// ... inject classname and styles to the page
const className = [uniqueClassName, props.className].join(' ');
// Return the component with the generated className
return React.createElement(Component, {
...props,
className
})
};
};
}
When we render DangerButton
, the Component given to the styled function is our Button
component. DangerButton
will generate a unique classname (based on its own styles / given variants), and pass that as the className
prop to Button
(a different styled function), which will pass its own, combined classname to the native button
element.
This is what also allows us to write components wrappers:
const Button = (props) => {
// css-b3fdea
console.log(props.className)
// We have to pass className for styling to apply.
return <button {...props} />
}
const StyledButton = styled(Button)({
color: red;
})
// css output
.css-b3fdea {
color: red;
}
Yep, I feel you.. recursion can be overwhelming sometimes.
Although it's great to understand how things work under the hood, there are two key concepts to remember when using styled functions:
- Each component gets its own unique className, based on its styles and props. These unique classNames are applied to the underlying DOM node being rendered.
- When we compose components and render the top-level one, we render all composed components down to the native HTML element, with each composed component adding its own unique className.
Nested Selectors
Styled functions not only generate unique class names but also return them, allowing you to create complex nested selectors. In its simplest form, styled function can implement toString to return their base class name, enabling nested selector patterns:
// Basic styled implementation
function styled(Component) {
return (styles) => {
const className = generateHash(styles);
const StyledComponent = props => {
return <Component {...props} className={className} />;
};
// Key part: implement toString to return the class name
StyledComponent.toString = () => className;
return StyledComponent;
};
}
// Usage:
const Card = styled('div')({
padding: '20px',
backgroundColor: 'white',
':hover': {
backgroundColor: 'gray'
}
});
const Button = styled('button')({
backgroundColor: 'blue',
color: 'white',
// Card.toString() is called here, returning its className
[`.${Card}:hover &`]: {
backgroundColor: 'darkblue'
}
});
// Generated CSS:
.card-hash1 {
padding: 20px;
background-color: white;
}
.card-hash1:hover {
background-color: gray;
}
.btn-hash1 {
background-color: blue;
color: white;
}
// Button styles when Card is hovered. Applied only in the context of Card component.
.card-hash1:hover .btn-hash1 {
background-color: darkblue;
}
Whenever you use ${Card} in a template literal, javascript automatically calls toString(), returning the component's className.
Drawbacks of runtime CSS-in-JS
Now that we have a basic understanding of how runtime css-in-js works under the hood, what about its biggest drawbacks?
- Runtime processing overhead: When components render, the styled function must transform javascript styles into css and inject them into the document. This process happens on the main thread (Web Applications are granted a single thread, the Main Thread, to run most of their application and UI logic), impacting your application's performance.
- Browser performance cost: Each style injection forces the browser to recalculate styles and layouts. This constant manipulation of the CSS engine during runtime triggers expensive browser operations, affecting the critical rendering path. Unfortunately, this is not fixable in runtime css-in-js due to the nature of how it works.
Zero-Runtime Libraries
Unlike runtime css-in-js, zero-runtime solutions process styles during build time. This approach can offer significant performance benefits, as the browser doesn't need to do any additional work to compute and inject styles at runtime.
There are two main approaches to achieving this:
The Static Analysis (AST) approach
This approach parses the JavaScript code into an Abstract Syntax Tree and extracts styles without executing the code:
const Button = styled('button')({
backgroundColor: 'blue',
color: 'white',
padding: '8px 16px'
});
// During build time, AST parser sees:
// - It's a styled function call
// - First argument is 'button'
// - Second argument is an object with static values
// - Generates: .btn-hash { background-color: blue; ... }
The biggest drawback of AST analysis is that it can't handle style interpolations or nested selectors. This is because during static analysis, we don't know what the generated class names will be! Consider the following example:
// ❌ Won't work with AST analysis
const Button = styled('button')({
padding: '20px',
'&:hover': {
backgroundColor: 'gray'
}
});
const Card = styled('div')({
backgroundColor: 'blue',
[`${Button}:hover &`]: { // We don't know Button's class name yet!
backgroundColor: 'red'
}
});
Or dynamic styles:
import { lighten, darken } from 'polished';
// ❌ Won't work with AST - can't execute functions
const Button = styled('button')({
backgroundColor: '#1e88e5',
'&:hover': {
backgroundColor: lighten(0.1, '#1e88e5') // AST sees a function call but can't execute it
}
});
The Node VM Execution approach
This approach actually runs your styled components code during build time. This involves transpiling the code (converting React code to vanilla JS, for example), setting up a environment, and executing it to extract the styles:
// Your source code
const Button = styled('button')({
background: theme.colors.primary,
padding: theme.spacing(2),
borderRadius: '4px',
'&:hover': {
background: darken(0.1, theme.colors.primary)
}
});
// During build time:
const vm = require('vm');
// 1. Set up the context
const context = {
theme: {
colors: { primary: '#1e88e5' },
spacing: (unit) => `${unit * 8}px`
},
darken: require('polished').darken,
// ... other required globals
};
// 2. Create a sandbox
const sandbox = vm.createContext(context);
// 3. Execute the transpiled code
vm.runInContext(transpiledCode, sandbox);
// 4. Extract generated CSS
// Output:
.btn-hash {
background: #1e88e5;
padding: 16px;
border-radius: 4px;
}
.btn-hash:hover {
background: #1976d2;
}
This approach solves the limitations of AST analysis:
- Can execute functions (like
darken
from polished.js) - Can handle nested selectors
- Supports style interpolations
However, this approach comes with its own challenges:
- Need to mock browser APIs (window, document, etc.). This is solvable by using libraries such as jsdom / happydom etc
- Slower build times due to code execution
- Must handle dynamic imports and external dependencies
- Security considerations when executing code
How styles are attached to components in zero-runtime approach?
Unlike runtime CSS-in-JS where styles are processed and injected during render, zero-runtime solutions do all the heavy lifting during build time. The styled function that ships to the browser is remarkably simple:
const styled = (Component) => (styles) => {
// Tiny hash function that matches build-time hash
const getStyleHash = (styles) => {
const hash = calculateHash(styles)
return `css-${hash}`;
};
return function StyledComponent(props) {
const className = getStyleHash(styles);
return <Component {...props} className={className} />;
};
};
// Usage remains the same
const Button = styled('button')({
backgroundColor: 'blue',
color: 'white',
padding: '8px 16px'
});
// When Button renders, it calculates the same hash that was generated during build
<button class="css-5d7f96" />
That's it - just a hash function and a component wrapper.
All the heavy lifting of processing styles and generating CSS has already been done during build time, and the CSS has been extracted to a separate file:
/* styles.css - generated during build */
.css-5d7f96 {
background-color: blue;
color: white;
padding: 8px 16px;
}
All the complexity of processing styles, generating hashes, and creating CSS rules happens at build time. The runtime code that ships to the browser is just a thin wrapper that applies pre-calculated class names to components. This is why it's called "zero-runtime" - there's virtually no styling overhead when your application runs.
Consider this: when you open a new tab in your browser, it gets allocated to a single thread. All JavaScript execution, rendering, and other tasks must share this thread. By moving style computation to the build phase, we're freeing up valuable processing time on the client side.
So, what library should you choose?
Unfortunately, I don't have a definitive answer for you. As a developer, it's your responsibility to evaluate these pros and cons and make an informed decision based on your project's specific needs.
If you care about the load performance of your site, and especially web vitals, runtime libraries might not be a good fit for you and your team. Less javascript = faster user experience. There isn’t much we can do about it.
However, performance isn't the only factor to consider. Design systems and component libraries are becoming increasingly complex, and managing design tokens, themes, and component variants is a crucial part of modern web development. Simplifying the mental model around using a library is where the developer experience becomes just as important as performance metrics.
What's Next
Thanks for reading! This dive into css-in-js internals emerged from the research I did prior to developing jux toolkit, our zero-runtime css-in-js library. Mapping the pros and cons of each approach in the ecosystem (including the developer experience around using a library) was a challenging task, but with jux toolkit, we're trying to provide a cohesive solution that balances performance with developer experience. I encourage everyone to check it out, and feel free to reach out with any questions or feedback!
In Part 2, I'll cover in-depth how jux toolkit works under the hood and share our journey at jux, including what motivated us to create a new css-in-js library.