vanilla-extractAfter using vanilla-extract in a project for a year, my initial impression has completely changed.
At first, I simply considered it a "CSS-in-JS tool with type support." styled-components, Emotion's an alternative to, and I wondered what differences it had from Tailwind, which I was accustomed to.
Although I adopted it lightly, my perspective began to change as I encountered the structural limitations of existing CSS-in-JS in a React Server Component environment. The method of generating styles at runtime didn't align well with server components, requiring client-side specific handling or complex workarounds. Structural fatigue gradually accumulated. In such a situation, vanilla-extract's runtime-free static styling structure felt like a realistic alternative beyond a simple tech choice.
This article comprehensively covers vanilla-extract's philosophy, internal structure, type inference method, build flow, and performance characteristics.
Instead of a simple introduction to its usage, I aim to explain why this structure was chosen and in which situations it offers strengths, based on actual project experience and technical context.
In the process, the following questions naturally arose:
Why do existing CSS-in-JS methods cause problems in server components?
vanilla-extractHow does vanilla-extract implement type safety?
sprinkles, recipe, createThemeHow do the internal structures of sprinkles, recipe, and createTheme work?
What CSS is actually generated when looking at DevTools and bundle results?
What are the fundamental differences when compared to Tailwind, Emotion, and Stitches?
Since these questions are difficult to cover at once, I plan to delve deeper into each of them in subsequent articles in this series.
Evolution of Styling Tools and Limitations of Existing Methods
In frontend development, styling isn't just about "decorating the screen." In reality, it significantly impacts the overall design, including performance, maintainability, scalability, and collaboration structure.
vanilla-extract is not just another styling tool. It's closer to an attempt to structurally solve the limitations of previous methods, addressing each one individually.
Traditional CSS
Being global in scope, classes created in one file could conflict anywhere, and
class names had to be manually chosen, making consistency difficult to maintain, and
it was easy to override styles without a clear design, leading to increasingly tangled structures.
/* 너무 유연한 구조 */
.button {
background: red;
}
While fine for small pages, as the scale grew, style management became increasingly complex.
CSS Modules
It prevented conflicts by creating a file scope per .module.css unit, and
Webpack automatically hashed class names, preventing global pollution, but
it was still separated like a distinct DSL from JS/TS, leading to a disconnect in type/autocomplete.
/* Button.module.css */
.button {
background: red;
}
/* Button.tsx */
import styles from './Button.module.css';
<button className={styles.button} />
- Class names were treated only as strings, and IDE autocompletion was difficult to expect.
- It was a structure where the structure or meaning of CSS values was difficult to manage directly at the component level.
While useful for small teams, it was difficult to extend to design systems or type-based development.
CSS-in-JS (styled-components, Emotion, etc.)
Styles were written within JS, becoming fully integrated with components, and
it became flexible for props-based conditional styles, theme manipulation, and dynamic configuration.
const Button = styled.button`
background: ${props => props.primary ? 'blue' : 'gray'};
`
However, this flexibility came at a significant cost.
- As execution contexts increased, managing both debugging and bundle performance became burdensome.
- As the number of components grew, CSS generation also increased proportionally, leading to cost increases without optimization.
While it became possible to write styles more flexibly and conveniently,
it became even harder to predict which styles would apply and when.
🔗 Related Resources:
- The dark side of CSS-in-JS — Max Stoiber
- Why we dropped styled-components — Sentry Team Blog
Utility-first Systems (TailwindCSS, WindiCSS, etc.)
- UI composed solely of predefined class combinations
- All styles are generated statically, with no runtime cost.
- Unused classes are removed by PurgeCSS → Minimized output.
<!-- 클래스명만 보면 역할이 드러나지 않음 -->
<div class="p-4 text-blue-700">...</div>
- Class naming conventions are enforced, requiring extension at the config file level.
- Lack of a type system makes it difficult to catch typos or errors at compile time.
While optimized for rapid UI assembly, it's difficult to abstract from a design perspective, and design system scalability is limited.
Thus Emerged vanilla-extract
vanilla-extract starts from the following questions:
❓ Why should styles exist outside of JS/TS's type system?
❓ Can static styling systems and design flexibility not coexist?
❓ Can design tokens not be clearly expressed and controlled in code?
The answers to these questions led to the emergence of three concepts:
- Declare styles in
.css.tswith TypeScript - Statically separated into
.cssduring build, resulting in 0% runtime cost. - Type system directly involved in style definition
// 완전히 타입 안전한 스타일 시스템
const button = recipe({
variants: {
size: {
sm: { fontSize: '12px' },
lg: { fontSize: '18px' },
},
},
});
- Usage of properties other than those defined is impossible (autocomplete + type errors).
- CSS variables are also declared with
createThemeContract()→ Theme switching is also handled type-based.
vanilla-extract is not merely a tool for writing CSS.
It's a method that allows you to safely embed a design system in code with types.
It's fast, prevents errors with types, and allows for clean styling architecture.
It can be called a new styling method that combines all three strengths.
vanilla-extract's Philosophy and Design Goals
Written by quoting an interview with Mark Dalgleish, co-creator of CSS Modules.
This tool originated from a philosophical attempt to solve the two fundamental problems of runtime cost and type mismatch that the CSS-in-JS ecosystem has faced.
Behind it lies an old question: “Can styles be managed more structurally and safely?”
"I'm still trying to come up with new ways to rewrite CSS. For some reason, I can't let it go."
— Mark Dalgleish
Looking at the tools Mark has created, such as CSS Modules, Braid, and vanilla-extract ,
the consistent concern “How can CSS be managed more structurally?” is apparent.
The solution he presented in vanilla-extract is not merely performance improvement or type complementation.
It is a structural approach to make design systems predictable and safely extensible.
Let's summarize vanilla-extract's design philosophy, focusing on the 3 core principles it embodies.
1) Runtime-free Styling System

Existing CSS-in-JS methods operate styles within JS, so
styles are not applied until JavaScript is executed in the browser.
The main problems with this structure are as follows:
- Delayed initial rendering in CSR environments
- Flickering occurs in SSR environments
- Difficulty debugging in DevTools
- CSS included in JS bundle → Increased bundle size
"It has the same performance characteristics of something like Sass… and at runtime there’s really no work left to do."
— Mark Dalgleish
vanilla-extract structurally blocks this problem.
All styles are statically extracted from .css.ts → .css at build time, and the browser only loads pure CSS files.
The advantages of this approach are clear:
- Improved initial rendering speed (0% style generation cost)
- Separation of JS and CSS bundles → Tree-shaking optimization
- Hash-based class names → No conflicts
- Easy class tracking in DevTools
Thanks to the static CSS structure, understanding the style architecture becomes easier.
2) Style Control via Type System
"If you bet on the JS ecosystem… suddenly you're writing your styles in a type-safe language."
— Mark Dalgleish
A common problem when writing CSS is that typos or incorrect values only become apparent after execution in the browser.
vanilla-extract solved this with TypeScript.
- Incorrect style input → Immediately detected as a compile error
- Autocompletion → Can only be written within the scope of design tokens
- Disallowed values are completely blocked from input.
In particular, the combination of defineProperties() and createSprinkles() allows for enforcing design system-based style properties with types.
const Box = (props: { padding: 'small' | 'medium' }) => (
<div className={sprinkles({ padding: props.padding })} />
)
"My editor is telling me that I’m getting my styles wrong."
— Mark Dalgleish
3) Design System = Token System
Existing CSS variables are mostly declared globally. However, this also comes with the disadvantage of difficult dependency and traceability.
vanilla-extract is designed to limit the scope of even these variables to module units and control them with types.
"We want scoped variables as well. So if you want to use that brand color variable, you have to import it from the thing that created it."
— Mark Dalgleish
createThemeContract()– Define token schemacreateTheme()– Map actual values to CSS variablescreateGlobalTheme()– Define global themethemeClass– Provide root class per theme
The advantages of this structure are:
- Explicitly manage design tokens and map them to CSS variables
- Dark mode switching only requires changing classes
- During collaboration, design rules can be shared type-based.
"In trying to take a step forward, we've actually taken a big step back." — Mark Dalgleish
In conclusion, vanilla-extract is a tool for handling CSS statically and structurally.
"In a perfect world, I should never have to write CSS."
— Mark Dalgleish
In reality, custom styles are ultimately inevitable.
However, if even those moments can be controlled within a clear scope and type system,
design systems become more predictable and more extensible.
"It’s not that CSS-in-JS is bad. It’s just that the platform caught up."
— Mark Dalgleish
Ultimately, what matters is not “what you use”, but “how you structure it.”
vanilla-extract is a tool for teams contemplating such structures.
It allows for statically declaring design systems and safely extending them based on types.
"If you agree with the philosophy, then you should definitely use it."
— Mark Dalgleish
✍️ Concluding Remarks
vanilla-extract is more than just "CSS-in-JS with type support."
It allows for statically and structurally declaring styles and clearly controlling design systems with types.
- Faster rendering
- More stable code
- Clearer collaboration structure
If you want to achieve all three simultaneously, vanilla-extract is worth a thorough look.
The core concepts covered in this article will be explored in more depth in subsequent series.
References
https://kentcdodds.com/chats/04/22/mark-dalgleish-chats-about-vanilla-extract
https://x.com/markdalgleish/status/1375332726664953860?utm_source=chatgpt.com
