In this Astro vanilla-extract extract post, we start by taking a look at what vanilla-extract is. Then we turn to some code from a working Astro vanilla-extract project. This will help us see how to set up vanilla-extract to work with Astro. As well as that, we will see some vanilla-extract features, in case you are less familiar with vanilla-extract itself. Once we have the global styles set up, we see how you can use vanilla-extract in Astro components as well as Svelte ones. Astro supports multiple component libraries, and the code will also be useful if you work in Preact or some other frameworks. Anyway, the easiest way to learn about vanilla-extract is probably just to roll your sleeves up and spin up a project. With that in mind, let’s get going as quick as we can!
already know how much time the associated tooling can save you. Writing your styles in
vanilla-extract brings similar advantages; you get Intellisense autocompletion and on top if you
spacing-xs in your global styles, then the editor
will flag up an error if you try to use
spacing-xs. This can
save you a lot of time in debugging styles.
On top, vanilla-extract lets you define contracts for themes. We see later that setting this up,
we can stipulate that any theme needs to have certain properties defined. Examples might be
fontFamily. This is great if you tinker away on your site
on the dark theme, refactoring chunks, and forget to keep the light theme in step. The editor
will have your back!
Some CSS-in-JS libraries can come with a performance hit as they have a runtime overhead. Another advantage of vanilla-extract is that although you author your styles in TypeScript, we will see Vite compiles these to vanilla CSS. That vanilla CSS is what we ship to the end-user browser. For that reason, this criticism sometime levelled at other CSS-in-JS libraries does not apply to vanilla-extract.
We will use example code from a newsletter page. We won’t build it from scratch, instead, we pick out the most important details for quickly getting up to speed with Astro vanilla-extract. In summary, we will see:
- how to use vanilla-extract in Astro and Svelte components
- an example of theme contracts,
- a way to use vanilla-extract classes to implement a dark/light theme toggle.
Enough talk! If you are still interested, let’s start by seeing how to set up vanilla-extract in Astro.
There is not yet an Astro integration, but the setup is far from onerous. If you are starting from scratch, spin up a new Astro project to get going. Once you have an Astro project to work on, add the vanilla-extract packages:
Next, update your
astro.config.mjs file in the project root
directory to use vanilla-extract:
That’s it, we’re all set. Before adding global styles, we will look at adding a theme.
I created a
src/styles folder for vanilla-extract global styles
and, themes as well as vanilla CSS for self-hosted font font-face directives. The themes are in
src/styles/theme.css.ts. Remember, we write vanilla-extract styles in TypeScript and Vite transpiles these to vanilla
CSS for us. Here is the theme code (
We mentioned earlier that the theme contract is just a way of making sure whenever we create a new
theme, that we define all fields which should be defined. To set this up, we import
1). Then define the contract in lines
40 by calling
createThemeContract. Then for each theme we call
createTheme. If you are coding
along, try omitting a field in a theme you create or even adding an extra field to the contract to
see the editor response!
The site we build is fairly simple, and you can add
900 colour ranges instead if you are
working on a more substantial project. Of, course you can add an extra contrast or other themes,
all linked to the contract. Next, we use these themes, while setting up global styles.
So, to use the theme, first we import it (line
2) and then we can
just pull off the particular field we need. For example, in line
14, we set use
theme.fontSize.size1 as the
font-size for the body element. This will pick
size1 based on whichever
theme is active in the browser. In our case, it’s the same value for light and dark themes,
though we could add accessibility themes with different font sizing. Notice if we had not defined
fontSize.size1 in our theme, then we would have an error in the editor now. With vanilla CSS or even some
other CSS tooling, we would be none the wiser!
33-35 you see the syntax for adding selectors. This is
Finally, we can define we can export a group of styles as in lines
49. These are screen reader styles which we will use on a button. We will see we can use the exported variable (
screenReaderText) as a class attribute value on a DOM element. That applies this set of styles to that element.
That probably sounds more complicated than it is. It should be clearer when we see the button
Our newsletter site uses Markdown for the newsletter content. We just have one edition 😅. The
source is at
src/pages/index.md. This (and future newsletters)
will use the
src/layouts/BaseLayout.astro code as a layout. This
adds the HTML head, imports styles and so on and so forth!
to render the page. Importantly, we import
src/styles/global.css in line 6. We are using import aliases and so can shorten the path to
~styles/global.css. We skip the
32 we are applying a
container class and a
lightThemeClass to the
body element. The light theme is the default and can be updated from the toggle button code. The Astro
class:list directive is syntactic sugar , letting us push all the classes we want to apply into an array. More interesting, though, is
where do these variables come from?
We import them in lines
In the screen capture, we have selected the
body element in the
left pane. The two classes,
_1lheu5d10, correspond exactly to the
container (which we will see in a
lightTheme class. In fact,
_1lheu5d10 is highlighted on the right pane, and you can see the fonts and (just above) the box shadow
values. I optimized the site build with subfont, which explains the
Work Sans font appearing as
Work Sans__subset. You can learn how to do
this with Astro in the video on Astro Self-hosted fonts.
As promised, next we see where we defined the
styles as well as the other styles in the
template. You will see the syntax is not too different from what we saw previously.
Here in lines
22 you see an example of a media query in vanilla-extract. Then in line
30 we target the footer text and increase
the font size. This looks a bit more involved, but shows how you can apply nesting. So we are targetting the
footer class nested within a
wrapper (defined above) class. This transpiles to something equivalent to:
So, on the
body element, we had a
lightThemeClass which we said was a default. Next, we see the code where we update this initially and also when
client:load directive in
BaseLayout.astro. Here is the button logic (
We use a Svelte store to keep track of theme. This syncs to local storage and can be handy to remember the preference for the user’s next visit to the site. We sketch over the details here, but we see exactly this use case in the Svelte Local Storage video.
When the component first mounts, we check what the theme should be (lines
19). The default is light, so if the user
prefers dark we just replace the
lightThemeClass attribute on the
body element with
handleClick function essentially does this task when the user
clicks the toggle button. It swaps between
darkThemeClass just depending on which theme is active.
The important takeaway is that we just need to swap the
lightThemeClass for the
darkThemeClass to change theme in the browser.
Although it is not much of an abstraction to apply theme state code to Preact or other libraries, let me know if you would like to see a Preact working example using Signals to manage state as an example.
Finally, we see the
screenReaderText class in action in line
34. So all we had to do was import it from the global styles file, then place it on the
In this post, we saw how to add Astro vanilla-extract styling. In particular, we saw:
- how to use the setup vanilla-extract for Astro,
- how to create theme contracts for more maintainable and robust code,
- adding a dark/light theme toggle using vanilla-extract and local storage.
We only scratched the surface of what you can do with vanilla-extract here, and there is a fantastic in-depth tutorial by Lennart if you are hungry for more.
You can see the full code for this project in the Rodney Lab GitHub repo . I do hope you have found this post useful! I am keen to hear what you are doing with Astro and ideas for future projects. Also, let me know about any possible improvements to the content above.
- Yes! Vanilla extract works just fine with Astro. At the time of writing, there is no integration, but setup is so simple, you should be able to cope without. First, add two vanilla-extract packages: `pnpm add -D @vanilla-extract/css @vanilla-extract/vite-plugin`. Then, import `@vanilla-extract/vite-plugin` in your `astro.config.mjs` file and add it to the Vite plugins array. Happy styling!
- Some CSS-in-JS libraries come with a performance hit at runtime. vanilla-extract is not in this group, though. Working with Vite tooling, you can write style in TypeScript. Vite transpiles that TypeScript into regular CSS. Only the vanilla CSS gets shipped to the browser, so there is no runtime overhead. With vanilla-extract you can put style in separate files and link them to the DOM using classes (much like vanilla CSS). TypeScript authored styles can save you a lot of time debugging; undeclared or badly typed variable names show up instantly. You get an improved developer experience without the user on a slow connection having to pay for it!
- Debugging CSS can be a headache, especially if you are working on someone else’s code or even your own code written a while back! Contracts in vanilla-extract have you back here. They can be used to make your CSS more robust and maintainable. From the outset, you can stipulate that themes must contain `primaryColour` as an example. Now, because under the hood vanilla-extract uses TypeScript tooling, any new themes missing `primaryColour` will get flagged up as an error in the editor. This also goes for if you add a new, ad-hoc variable to the dark theme without updating the underlying contract. Hopefully you will be setting red borders on elements much less often going forward 😅.
Have you found the post useful? Would you prefer to see posts on another topic instead? Get in touch with ideas for new posts. Also, if you like my writing style, get in touch if I can write some posts for your company site on a consultancy basis. Read on to find ways to get in touch, further below. If you want to support posts similar to this one and can spare a few dollars, euros or pounds, please consider supporting me through Buy me a Coffee.
Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via @askRodney on Twitter, @[email protected] on Mastodon and also the #rodney Element Matrix room. Also, see further ways to get in touch with Rodney Lab. I post regularly on Astro as well as SEO. Also, subscribe to the newsletter to keep up-to-date with our latest projects.