Converting to Vanilla Extract
I've recently been working with and learning about Vanilla Extract, a type-safe CSS-in-TS library that outputs static CSS at build time. Generally, companies are shifting away from the CSS-in-JS libraries like styled-components that are driven by runtime for the performance gains and type safety.
I wanted to put Vanilla Extract to the test, so I've converted this website - a Next.js blog - from CSS Modules and over to VE.
Next.js support
So Vanilla Extract does have pretty good out-of-the-box support for Next, which powers this site. Using the @vanilla-extract/next-plugin
package, we can pretty quickly get setup. Their docs also provides easy to follow steps.
In short, we're just adding a new plugin into the next.config.js
file, which in my case will wrap my useMDX
plugin that is used to render the blog posts.
Once we upgrade Next to the latest version though (which is recommending usage of Turbopack) things start to fall over a bit.
Turbopack
So Turbopack doesn't support Vanilla Extract, at least not right now. Not only is this called out in a VE github issue but is also called out by Next themselves.
Given this has been ongoing since at least March of 2024 (according to the GitHub issue) I think it might be a while until this issue is fixed, if at all.
While this isn't an immediate problem. I am expecting Next to slowly migrate over the next few versions to Turbopack for all users.
Converting from CSS Modules
When I orginally created this site using the create-next-app
tool, it used CSS Modules. Converting from CSS Modules hasn't been overly difficult, but one thing that struck out was the inability to style 'at a distance'.
For example, the below CSS logic no longer works in a Vanilla Extract world
.className p {
color: 'blue';
}
Instead of setting this up in a .modules.css
file and forgetting, we need to style this from the child, not the parent.
So for setting up something like my blog posts page, where I don't know what the content is going to be, it becomes tricky.
To give you a real-world example, I previously had a grid setup for the links displayed at the bottom of each blog post which you should see if you read this post to the end 👀 This is what the code looks like in a vanilla-extract world:
export const footerStyles = style({
display: 'grid',
gridTemplateAreas: '"a a a a" "b c d ."',
gridTemplateRows: 'repeat(3, 1fr)',
textAlign: 'center',
'@media': {
'(max-width: 600px)': {
gridTemplateAreas: '"a" "b" "c" "d"'
}
}
})
export const footerLink = style({
selectors: {
[`${footerStyles} &:nth-child(1)`]: {
gridArea: 'a',
},
[`${footerStyles} &:nth-child(2)`]: {
gridArea: 'b',
},
[`${footerStyles} &:nth-child(3)`]: {
gridArea: 'c',
},
[`${footerStyles} &:nth-child(4)`]: {
gridArea: 'd',
}
}
})
So this new footerLink
is a new style class I have to create and use for my links, rather than doing something like .footerStyles a
Keeping that in mind, I like the fact that you are now acutely aware of each style that is applied to your components. This new way of thinking means you're far less likely to introduce a regression by blanket applying styles from a base style file, as global styles now need to be explicitly created via the createGlobalStyle
api.
Dark mode
Dark mode is an interesting one. You'll notice I've recently added a button on the top navbar that lets you toggle it.
Give it a try! 🌑
How does it work? I've got a provider and context setup to share the state in the React side of things.
But we no longer live in a world of being able to dynamically pass in data, since the CSS is compile time now.
So in my global theme file, I setup a lightModeTheme
and a darkModeTheme
which both honor a theme contract created via createThemeContract
.
I believe both themes are compiled down to the CSS, and it's just a matter of applying the className at the right time.
There is probably a better way of doing this, but in a useEffect
my dark mode provider simply does this:
if (isDarkMode) {
document.body.classList.remove(lightStyle)
document.body.classList.add(darkStyle)
} else {
document.body.classList.remove(darkStyle)
document.body.classList.add(lightStyle)
}
Opinions
Ultimately, I find working with Vanilla Extract pretty pleasant. I enjoy the eslint plugin's recommended settings, as it makes you consider specificity and concentric ordering.
It also gave me an excuse to actually create a color palette for this website, which was previously almost 50 different shades of grey. On the color palette, if you're interested in creating your own I used the Radix custom palette tool.