Vanilla Extract: CSS Preprocessing in the Age of Components

All right.

Thank you for that intro.

so as you heard, my name is Mark Dalgleish and if you're at all familiar with my work, which has just been spoken about, I am obsessed with maintainable CSS, but particularly doing so via build tooling because I think historically, it relied too much on everybody knowing the magic incantations and naming conventions to make sure that our code stayed maintainable.

But I think that it's possible for us to do it, to do a lot of the work automatically with build tooling, hence CSS modules and other similar tools.

But beyond the, the tooling at the low level, I also think an important part of this story is how we then scale our CS CSS upwards with design systems.

So I've done a lot of work in that space as well, building out large component systems on top of this tooling.

Now design systems are built up of many layers.

Of course, at the lowest level you might have something like this, some JSON, design tokens, and we're, this is just a subset of what you would have in reality, you've got your color palette, space, scale, and so on.

And then of course, you might layer up in terms of, some CSS variables and you're defining those that are available throughout your style sheets.

You can update these dynamically and so on, but then you have this dependency from there to your styles.

Now your styles depend on these CSS variables that you've defined, and so there's a relationship now from your tokens to your theme variables, to your style sheets.

You may have utility classes that take this further and, basically enumerate all of your colors and all of your space scales and so on into reusable classes.

And then you get into your markup, and now your markup has a dependency on these classes that you've created.

And so all of these files need to talk to each other implicitly, if I'm writing it this way, for everything to work correctly.

But if you are used to working in TypeScript in the rest of your application, the question quickly arises here around type safety.

How do I know that all of these symbols and names for things that I'm writing all over the place, how do I know that these all line up correctly?

Obviously, I can refresh the page and just check that it's broken or not, but.

We've really moved beyond that when it comes to our JavaScript tooling, and ideally we want to get more of that into our styling as well.

Now, of course it goes without saying that writing your CSS in TypeScript as a lot of people, do, makes this easy because now you're in that same language.

You get the same, type primitives that you have with the rest of your code.

as a simple example of how you, of one of many ways that you might achieve this is that let's say we have our button component and we wanna have multiple sizes available that we wanna set, but we wanna do this in a type safe way, right?

So what we're gonna do is we are first gonna create a, an object here called sizes.

And it's going to describe all the valid sizes for my button component.

So I've got small, medium, large.

Each of them contains an object of styles that I wanna set.

What's nice about having it in this data structure is we can then feed this into our type signature for the button props.

We can say that the button props has size available, and we're gonna inspect that object and say, Hey, TypeScript, give me the keys of this object.

Those are the valid values for this prop.

So then we go down to our runtime code.

We've got our size prop coming in.

We've got TypeScript behind us now, ensuring that this size is one of the valid values of small, medium, large coming in.

And we can then safely do a look up on this object and say, gimme the styles for that, and let's just spread that into the CSS prop.

And on the fly, we'll generate some styles.

Everything's great, everything's type safe.

It's amazing.

However, we've just added a runtime to achieve this.

So that means now first of all, our bundle size of our JavaScript has increased, a bit.

And it also means that rendering our component takes a bit more work.

Now this may or may not be an issue depending on what you're doing, but on the other extreme, some people are in very constrained environments where they're trying to keep the bundle size to a minimum.

They're trying to re, increase performance as much as possible.

Maybe you're shipping a library, right?

And you really don't want to add all these layers of, libraries and runtime code to it.

So you wanna keep it nice and lean.

So it seems like we're being forced to make this trade off between, the, our development experience and the safety that we expect in the rest of our JavaScript code.

We want that in our style sheets as well.

But in order to achieve this, we've had to make a big trade off on architecture and the runtime characteristics of our code base.

And maybe this isn't a trade-off we would like to make, and obviously there's been a lot of people interested in this problem and trying out different things.

Today I'm here to talk about vanilla extract and I would say our very unique take on this problem.

In many ways, going back to the drawing board and some of the classic ideas around CSS and myself and others have been working on this for a while, and hopefully, you can take some of these learnings into your own work.

Now, as you heard, I've worked on CSS modules before.

In many ways, vanilla extract is the spiritual successor to CSS modules.

It's come from the same lineage, of course, but it's also highly influenced by our own work with CSS modules and hitting up against the limits of it at scale in a large design system.

Trying to keep it maintainable, gets very difficult beyond a certain point, as I'm sure you can imagine.

Now I talk about architecture.

It's very much the same architecture in the same vein as CSS modules and, Sass modules, as some people call it, if you plug in a CSS pre-processor.

So in the same way that you might have this example.

module.css file, or equally you might have a, module.scss file.

this slots into that same structure where you've got a, interestingly, a .css.ts file.

The interesting idea that we're presenting here is what if we use TypeScript as our CSS pre-processor rather than a custom language, Sass or Less.

what if we use the same language as the rest of our code base and get the type safety benefits that we've come to expect elsewhere?

So what does this look like?

We're gonna write a style sheet.

Interestingly, again, it's written in TypeScript, but you can talk about it as a style sheet.

You can think of it as a style sheet.

Here we are writing example CSS dot ts.

We're gonna import the style function from vanilla extract.

And now as a basic hello world, we're gonna export a class name and we can name this whatever we want.

And here we call the style function.

We pass in a description of here's the style that we want to, we want attach to this class name.

And there we go.

That's our basic, CSS file style.

Our basic style sheet.

Now, importantly, we've gotta call this out and really make this crystal clear up front.

This file is executed at build time.

This code does not run in your user's browser in the, again, in the same way that Sass does not run in your user's browser.

This happens to be TypeScript.

Yes, but it's the same architecture, right?

It's purely at build time.

So then the question naturally is gonna be, what does this compile to?

If I'm writing these TypeScript files, what's actually going into my user's browser?

So the interesting concept here, if you're familiar with browsers, is we're, sorry, with bundlers, is that we're actually behind the scenes generating a couple of virtual files that we send to your bundler.

So on one side of the equation, we've got the JavaScript code that will actually go, into the, JavaScript bundle, but we're also generating a static CSS file that we're sending into your CSS bundle.

Now if we look at the generated JavaScript, this is what we get.

Now what you'll notice is the import from our NPM package is gone.

That style function is gone.

It's literally just serialized the exports as they were computed at build time into the final JavaScript file.

So this is the JavaScript code sent to your users.

It's literally just export the class name and it's a hashed class name.

So if you've worked with CSS modules before, very similar, approach here.

I will say in development, you can have debuggable class names that tell you like which file it came from, what the name of the class name was, but then in production you get these nice minified, hashes, which is really nice.

So importantly, and this is the key point, is we're also generating this static CSS file.

So it's got that hashed class name and it's got the styles that we described in our CSS TS file.

This is sent as static CSS to your users.

They can open the network tab, check out that style sheet that's coming out, and you will see the style sitting right there, statically.

Now, how do we consume this in our application?

Again, if you've used CSS modules before, this is very similar.

We're going to, in our example component here, we're gonna import star as styles from example.css.ts.

So here, all of the exports, in this case, just the single export are coming out as this styles object.

So then our component now can access this.

So we're gonna say that this div in our example component is gonna render a class name of styles dot class name.

So remember again, that was exported from the other file.

Now already there's an immediate, benefit here over plain CSS modules in that because both files are written in TypeScript, TypeScript doesn't, TypeScript has no idea about vanilla extract.

It just sees TypeScript files talking to each other, right?

So it doesn't know that there's some magic build process.

It just sees these two files referencing each other.

It knows that this file is exporting a class name.

It knows that this file is importing it and it's able to validate that if you have a typo, for example, it's gonna tell you, Hey, there is actually no class name in that style sheet.

and so obviously a typo is a contrived example.

But a re a more realistic example might be that you refactored your styles, you've moved things around, and that class got renamed, it got deleted, whatever it happens to be.

It's very nice to have your, react component in this case telling you, Hey, that class name actually is gone now, by the way.

and this is, again, this is the sort of thing we've come to expect in the rest of our code base as we've moved as, an industry from JavaScript to TypeScript.

Now we're getting the same thing in our style sheets and the references between the different parts of our application.

What I wanna call out obviously this is the React, ecosystem track, but it's worth, calling out that this approach, just like CSS modules and Sass modules and well, CSS and Sass for that matter, is framework agnostic.

we plug into all sorts of bundlers, and it's pretty trivial for us to make new ones in the future if new Bundlers come along.

but importantly as well, it plugs into whatever, framework you want to use or even no framework.

If you just wanna write vanilla JS or a plain JavaScript library that writes inner HTML strings or does basic DOM manipulation, you can plug this right in as well.

So there's really no, no coupling to a framework at all.

So, far what we've seen is that it's very, basically it's, I've written a CSS module, but in TypeScript.

But the point here is that this goes much further than CSS modules and allows us to do things that were very difficult when we're working with plain CSS.

One notable difference you might have caught on so far is that the exports from your style sheet are now explicit.

So this is in stark contrast to CSS modules where every class name that you write in that file is just implicitly becomes an export, and they're all in the same flat namespace.

But often our styles are more complicated that, for a button there might be different, groups of, styles that we want to think about the sizes, the colors, and, so on.

This becomes trivial when we're writing our style sheets in a, in TypeScript.

Because here, what we're gonna do now is again, pull in the style function from vanilla extract.

But now what we're gonna do interestingly, is not just export the class name at, at the root of the exports, we're gonna export an object of colors.

And so this allows us to nest things and group things a bit.

Make our own name spaces.

Here we're saying all of our colors, basic example of primary and secondary.

Each of these is calling the style, function with the background color that we want.

But then we get to create a separate object just for the sizes, and we can say we want small and large setting different padding values.

So why is this useful?

Now that we have these explicit exports, we can have these custom data structures.

It means that now when we go over to our component code, so again, that's all purely static at build time, our component can now reach into these structures and make use of them.

So we're importing star as styles from button.css.ts.

But remember this now comes with multiple data structures coming out.

From a type perspective, this is really powerful because we can actually use these structures as the source of truth for our types.

So we can say that the color prop, the type of this is actually, we extract that from that object and we're saying key of type of styles dot color.

So TypeScript will give us a union type of all the keys of that object.

That is now the type of our props for this component.

So the style sheet is the source of truth for that.

Likewise, for the size, we're gonna say, give me those small and media, small and large keys.

Those are the only valid, values for this prop on this component.

But it's not just about types, of course, it means that it run time.

when we've got our color prop coming in, we've got our size prop coming in again, TypeScript is validating that these are correct.

We can now, scope the lookups, for these different, to, props.

We can say that for the given color value that they're asking for, whether that's primary or secondary, in our case, we're saying, go look up on this object.

Give me the class name for the primary color, or give me the secondary color.

And then you can also scope the size.

Gimme the class name for the, small size, the large size.

And this is something that if you're working with plain CSS, you would have to write a bunch of boiler plate code to map these things, and create these groups yourself.

But in this case, we're letting the style sheet be the source of truth for this grouping.

So to me, the goal of this is, so that when you get up to the component level, you get this kind of user experience, you type button color and you're going to, you're gonna get type checking and auto completion saying these are the valid colors, these are the valid sizes.

Again, with the style sheet being the source of truth.

And if you make a mistake, you type, the wrong thing doesn't exist or you've refactored something TypeScript's gonna tell you this is invalid.

But again, this is what we've come to expect from our components.

But importantly, all these types are being drawn from the style sheet itself.

The main way, the main change of thinking here is that your styles, you should think of your styles as data, not just a bunch of selectors and class names and setting colors and things.

You actually want to group things into objects so that you can do dynamic lookups based on those, you can use those as a source of truth for your type checking and do much more advanced things at runtime as a result.

This is especially important once you get into the world of CSS variables because, They become the foundation of very large systems that get used everywhere, right?

And, having type checking for this, it really takes it to the next level.

At the lowest level, this is what it looks like to deal with CSS variables in vanilla extract.

So here we're creating a theme, a basic theme, dot CSS dot ts.

For now, we're just gonna have a single variable just to show you the basics of how it works.

We're pulling in the create var function, and now similar to our class name example, before here, we're exporting a variable name.

We're saying accentColorVar is createVar.

Now that might seem a bit cryptic.

What does it mean to create a var?

what?

What actually are we doing here?

the question again is what does this compile to?

If we step down and look at the generated JavaScript, similar to our hashed class names, the same concept is being applied here for to our variable names.

So now export const accentColorVar is the string of this var expression.

So now if we put that we transpose this into a style sheet or even into our runtime code, we will get access to the value dynamically of this variable.

But interestingly, at this point, there's no generated CSS because as you'll note, we created the variable name in memory.

We have a reference to it, but we didn't actually set any styles against it.

we didn't put it into any actual styles, so there's no CSS coming out yet.

So let's change that.

Let's look at what it looks like to, for us to be setting the variable in our code base.

So now again, what we're gonna do is we're gonna create our accentColorVar with createVar, but at this point we're gonna generate some styles.

We're gonna use the global style function from vanilla extract.

The reason for this is it allows us to scope styles to an arbitrary selector.

This is a very common pattern to set default values for a theme.

So what I'm gonna do is set some global styles against the root of the document, and in here we're gonna set vars, or in this case just one var.

If this syntax is a bit cryptic to you, what it's doing is it's saying this is a dynamic key.

So I have a reference to this accent color.

I wanna set that as the key.

So this is the variable I want to set, and I'm just saying that the default value is blue.

So again, what does this compile to?

I think it's really helpful to keep in mind while we're writing this code, what's actually going to the user's browser.

The JavaScript is actually the same as before, cuz from a JavaScript perspective, we haven't created any new identifiers.

Literally all we've done is create this variable name in memory.

Our CSS now has this code that we've generated.

We've got the root selector, we're setting that variable.

Again, it's that hashed variable name.

And, we're setting the default value to blue.

So that's, the CSS that will go to your user's browser.

So how do we then go about accessing the variable, more realistically, throughout our component system?

so here we've got our button.css.Ts file.

So again, we're writing, this is a style sheet that we're writing.

We're pulling in the style component again, as we usually do.

Here we're pulling in.

This is what it's really interesting, I think, is we are having to explicitly import the variable that we want to access.

So these variables do not exist in the global scope, and you hard code the names all over the code base.

like any variable in JavaScript that you wanna share throughout your code base, you have to explicitly import it from, in this case, from theme.css.Ts.

And remember that accentColorVar is just a string of var bracket, hyphen you know, what you would've written by hand if it was a globally scoped, variable name.

So here we're gonna create a class, we call it root, the root class for our button.

We're calling style, and we're gonna set the background to be this accentColorVar.

So now our, application accent color is the background of this button.

And this is the foundation upon which we build up more realistically to full type-safe theming.

Not just one or two variables, of course, but entire collections of them.

So what we're gonna do now is use a high level API that's achieving a very similar thing, but setting a lot more variables via the createGlobalTheme function.

So this is coming from vanilla extract.

Now what we're gonna do is export, a vars object.

You can call this whatever you want.

Here we're calling it vars.

We're gonna call createGlobalTheme.

Now, similar to globalStyle, the reason it's global theme is because we are setting these values against an arbitrary selector.

So you could target anything, but as a, as a pretty, pretty standard default we're setting it against root.

And now what we're doing is actually passing in nested data structures of multiple, variable values.

So we're saying here, we, want our theme to contain color with primary and secondary, the default values being blue and aqua here we're gonna set up a space scale, similar to what we saw at the beginning of the talk as well.

We've got the small, medium, and large values.

And again, these are the default values that we're gonna write for our theme.

So what does this compile to now that we're doing a bit, there's a bit more complicated stuff going on.

The generated JavaScript again, is just gonna serialize that export.

It's just a big object that maps the human readable names that you use in development: color, primary so if you ask for vars dot color dot primary, it's gonna give you that variable name.

and so it, behind the scenes, it's essentially called create var for every leaf node of that object, it's given you a variable for primary and secondary.

It's given you a variable for your space scale of small, medium, and large, that's just taken care of.

And you don't have to remember these hash names, of course, you use those human readable names, those first class references in your TypeScript files.

The generated CSS flattens out.

CSS has no concept of nested variables like this.

It just creates a flat list of everything that you define with all those default values.

So nice and simple.

In reality, often one of the reasons why you would reach for CSS variables for your theming is because you wanna have multiple themes.

Perhaps, users can swap in and outta multiple light and dark themes or color themes.

Maybe it's even dynamic, maybe it's coming from the backend.

maybe you're building white labeled apps where every single, build has a different theme.

So there's lots of different ways that you could do this and reasons you might wanna do it.

Vanilla Extract has some really great tools for dealing with this.

So here, we have a lower level API called createThemeContract.

Now, what this is about effectively is saying I want to define the shape of my theme, but not actually generate any styles for it yet.

I want to have multiple implementations of this contract.

So it looks similar to what we've seen before where we're creating our vars object.

But the important difference you'll note here is that there's, there are no values provided.

the leaf nodes of this object actually don't matter.

They can be whatever you want, but null is a pretty standard, placeholder that people use here.

So it's just a way of saying that my theme needs to have these colors, these space scale values.

That's the shape of my theme in this system.

Okay.

And again, in the world of TypeScript, this is really useful, primitive to have, because now when I go to create a theme that is an implementation of this contract, what I can do is I can create a theme class here using the createTheme function.

So this will, this, if I put this class on an element, it will apply these values, right?

This is where it gets interesting, the vars object that was returned as part of my theme contract creation step.

I'm saying these are the variables I want to use, when I create this theme.

So all these values are gonna be assigned against those variable names, but importantly, TypeScript will validate that yes, this is a complete implementation of this theme.

So if I forgot a color, if I forgot one of my space scale values, or as I said before, you're iterating on your system.

We've gone and added a new color to our theme, TypeScript will tell you, Hey, this theme over here ha, actually has not updated to, to add the new value.

So this is where we get to really leverage, the abilities in TypeScript that we, again, we take for granted in the rest of our code base.

Now our style sheets can have that same structure as well.

Now, the next step, of course, now that we've set up our themes and maybe multiple themes against a single contract, is we want to go about accessing the theme in our components.

So if we go back to our basic button.css.ts example.

Now again, similar to our accent color, we're explicitly importing the vars object.

And so now when we go to set this root class on the element, we've got this nice nested data structure for accessing our themes.

We're saying the background is vars dot color dot primary, and if you've ever worked with TypeScript, you'll know that there's some nice side benefits of this as well.

as you type vars dot your editor is gonna suggest well, You've got color, you've got space, you're gonna have borders, box shadows, whatever it is, know -radius is whatever your theme contains, it's gonna suggest all of these things.

So in this case, we've got vars dot color, you hit color dot, it's gonna say, here's your color palette.

Do you want primary, secondary?

I often nest further than this.

So in systems I've built, before, for example, I'll split my colors into foreground and background colors, because I find that's a very distinction, but you might go further than that as well.

Same with padding.

You'll get vars.space.Medium.

It's gonna suggest all those values for you, so you don't have to remember, you don't have to hard code these things.

You've got TypeScript backing you up, and if you make a mistake, it's gonna be a type error.

The other reason that we want to reach for CSS variables, rather than just baking the values into the style sheets themselves, is we may want to go about updating the theme at runtime.

So as a contrived example, let's say we want to be able to have this theme component.

We wrap around any part of the application.

So we have the react layer here.

We wanna be able to pass in a custom color palette and say, for this part of the tree, these are the primary and secondary colors.

Now, you may wanna do this again because the user wants to dynamically change the colors on the fly.

Maybe these values are coming from an API call.

There, there's many reasons why you might wanna do this dynamically at runtime, right?

So vanilla extract gives us some tools to deal with this as well.

Notably, this now is in our theme component.

This, contrived theme, component example we're going with.

I've imported this vars object that contains our entire theme.

Now again, what we're gonna do is use the vars object as a source of truth for the types.

So if you think about, this component wants to accept a color palette, we wanna enforce that at a type level and say, you must gimme the entire color palette, or this is not gonna be valid, right?

So what I can do is I can say that, The colors prop of this component is a record of all the keys of my color palette, again, from the theme.

So this is saying this, the CSS file or the css.ts file is the source of truth for the colors in my system.

I want that to be the type of this that you have to give me all the colors mapping to a string value.

So then what I'm able to do safely at runtime is accept this color, colors prop coming in.

I've got this object describing a custom color palette.

And now what we're gonna do is use a, this is a runtime function.

So there is a small runtime package, optional runtime package in vanilla extract called dynamic.

You can think of it as being in the same ballpark of size and complexity as the, the famous class name package by, Jed Watson who's in the audience.

Thank you for class names, so this is like our version of class names, but for CSS variables it's like saying, What I wanna do is for a given, set of, variables, in this case, my color palette, I wanna assign all of my color palette to these values that have come in via the prop.

So it's saying, take these colors, write an inline style, attribute containing all of those CSS variable values.

And again, what's nice about this is TypeScript is gonna validate that you have in fact passed in all the colors, that you've asked for here from vars dot color.

Again, similar to our styles earlier.

The important thing to think about here, the shift in thinking is that your theme is data.

Your theme is not just a wall of CSS variables that you meticulously maintain.

there's some real structure to it and there's nesting in there that you can make use of in your types and at runtime.

And the more we can lean into this, the, more maintainable our systems can become.

Because you can start to do interesting things now where if your theme is data, you can map over your theme to generate styles.

So again, as a somewhat contrived example, let's say we want all of the ba, all of our color palette values to be available as background colors on the button.

So to do that, what we could do is in our button dot CSS dot ts file, we could import our theme object.

Here we're gonna create an object that we call background classes.

It's gonna contain in our basic example, primary and secondary, and maybe more in the future.

What's interesting to note here is we're pulling in an arbitrary NPM package.

We're pulling in lodash, we're using map values.

What map values does, it says, for this object, I wanna generate a new object, but with different values.

And you generate these values by running this function for every item.

So we're saying, take my color palette from the variables, from my theme variables, iterate over this, and for every value.

Now remember, the value is that string of the variable name, accessing the variable.

What we're gonna do is say gen return, a style.

So for every element in my theme, generate a class name with that as the background color.

So that's a little bit of heady logic going on there.

But basically what we're doing is just automatically generating this object by mapping over our theme.

We're saying in the JavaScript layer, I have this object where I can say, what's the class for setting the primary background color?

What's the class for setting the secondary background color?

And again, our CSS is just gonna enumerate all over those and spit them all out in this format with hash class names all the way through.

I won't get into too much detail on this, but the, this is such a common pattern that this is actually baked, this pattern's baked into vanilla extract, with the styleVariants function, it's the same idea.

The only difference is you don't have to explicitly call style on every item.

So it's just gonna generate class names for e for every element in your object here.

Now if you look at that and you think, this doesn't seem specific to buttons, that's a fair point because really what we are do doing here is creating utility classes a class for every background in our theme.

And I know for a lot of you, myself included, you might have a sour taste in your mouth when it comes to utility classes cuz you're used to accessing them through APIs like this, where you write these long strings of terse class names.

Basically.

That's a whole different language you have to learn.

And maybe at best you've got some, editor plugin that can validate that this is correct.

A lot of people really don't like this.

I've always been on the fence where it's I like the ideas behind it.

I'm just not necessarily a fan of the API.

What's interesting about vanilla extract is we can come back to the same architecture and really think about things in a different way.

So for example, here what I'm gonna do is pull some variables from my theme.

I've got the color and space, and I'm going to create a utils object.

I'm gonna nest this similar to what I did with my theme here.

I'm gonna, use style variance to map over all of my theme colors and generate a background class.

Then I'm gonna iterate over my space scale and say, give me a class with that space scale value as padding.

Give me a, collection of classes for the same space scale as margin in reality, you'll go further probably and have padding and margin of top, bottom, left, and so on.

Build up a full suite.

and you can even go, a level further and have responsive classes and say, I want a class for padding top small on mobile and a class of padding, top large on desktop, and so on.

And you can build up a data structure however you like of all these classes, and you can, programmatically build this from your theme, your CSS variables theme Object is the source of truth for these classes.

So the generated CSS obviously is gonna be like this for days.

hopefully not too long.

You don't put too many things in there, but the nice thing about this, as you might note, is that there's a lot of repetition here, and this compresses really well.

every single CSS TS file has the same hash at the start.

You'll notice that you're seeing 0, 1, 2, 3, 4, 5.

That's how the hashing works.

So it compresses, brilliantly.

The more you can generate in a single file, the better in terms of compression.

And so then at runtime, similar to what we saw with our, looking up our classes, this is just a, format I made up for this talk, but you could come up with other alternatives.

But here, I'm getting these classes by saying utils dot display dot block, utils dot background, dot primary.

I don't have to remember this sort of terse, custom language of class names.

I have type, safe access to these.

this is such a common thing that people want to do, that we actually have a library as part of the suite of vanilla extracts tools called Sprinkles.

I could do a whole talk on sprinkles, but I'll briefly touch on it.

sprinkles lets you effectively, generate that sort of system, for you and, it gives you a function for accessing them rather than that object.

So here I can say to my Sprinkles function, I want to have a class name that contains display block.

Background primary.

I wanna set some padding values.

Interestingly here, the, we have this, notation for conditional styles, where we are saying we wanna have mobile and desktop have different values of medium and large.

now importantly, all of this is configurable.

This is not like a framework that comes with opinions about what classes are available.

You can configure all the, keys and values here and all of the conditions that you want to use as well.

A lot of people go up one level, beyond this as well, and they wire that function up to a primitive component, like a box component so that they become props.

You can just say, box display, this display equals block and so on.

Now, in reality, of course, all it's doing is just sugar for giving you that long string of classes.

So again, we can let the compiler do the work of how do I, write this as a string of classes at dev time?

That's not how we think about it.

We think about it in this type, safe, structural way.

So what we are doing here with all these moving parts, what we're ultimately trying to do is build type safe primitives for design systems.

Building up in those layers from tokens to theme values, to theme classes, to styles, utility classes, up to our components and their props, and maybe even dynamic theming at runtime, and have types flowing all the way through it.

But still without those architectural trade offs we've had to make with other alternatives.

So as an industry, we've been moving more and more from JavaScript to TypeScript as we've realized the benefits of working at scale, with the ability to know statically that logically my code makes sense.

And it's what we're trying to do with vanilla extractors show that we can bring the same benefits to the world of static CSS files and pre-processors, and maybe have a second go at what do pre-processors mean with today's tooling?

So here what you're seeing is we've got that unified language benefit.

We've got JavaScript running everywhere, back end, front end.

Now it's in our style sheets as well.

our styles are now data and our themes are data that we can iterate over, map over type check against.

So that means we get that type safety that we've come to expect everywhere else, but ultimately without that trade off of shipping a runtime where you are just shipping plain CSS files to your users.

This is all in service of that goal.

I said that has been driving me for years, which is writing maintainable CSS and letting the tooling do the work to, get it at working at a low level, but let us work more efficiently at a high level.

And now with the type safety that we get from TypeScript.

Some people may look at all this and say, is this just you trying to avoid writing CSS or learning CSS?

I think of this as the complete opposite.

To be honest.

I think of this as saying I wanna go all in on CSS to the point where I want it to be the source of truth for my component system, and historical tools we worked in, in, in the past, we found we didn't have this often CSS was where everything was just a black hole of, dynamic stuff that could be wrong.

Now, we flipped around and said, you know what?

I actually want the types of my components and my, to be driven off my CSS variables and my style sheets.

So it's in my mind it's actually embracing CSS and just trying to take it to the next level, via these sorts of tooling.

hopefully there's something in there that's of interest to you if you're trying to build out the kind of systems that I've built in the past.

And, if you try it out and you like what or maybe you don't, I'd love to hear your experiences.

So please, talk to me about it.

I'd love to hear from you.

But otherwise, that's it from me today.

Thank you so much.

MAINTAINABLE CSS with BUILD TOOLING

SCALING CSS with DESIGN SYSTEMS

{
	"color": {
	"primary": "red",
	"secondary": "pink",
	"tertiary": "silver",

	},

	"Space": {
	"small": "8px",
	"medium": "16px",
	"Large": "32px",

	},
}
--color-primary: red;
--color-secondary: pink;
--color-tertiary: silver;

—-space-small: 8px;
--space-medium: 16px;
—-space-Large: 32px;
.Button_root {
	background: var(--color-primary) ;
	padding: var(--space-medium) ;
.bg_primary { background: var(--color-primary); }
.bg_secondary { background: var(--color-secondary); }
.bg_tertiary { background: var(--color-tertiary); }

.padding_small { padding: var(--space-small); }
.padding_medium { padding: var(--space-medium); }
.padding_large { padding: var(--space-Large); }
<button class="b¢g_primary padding_large">

TYPE SAFETY?

CSS-IN-TYPESCRIPT MAKES THIS EASY

// Button.tsx

const sizes = {
	small: { fontSize: 14, padding: 10 },
	medium: { fontSize: 18, padding: 16 },
	large: { fontSize: 24, padding: 24 },
} as const;

interface ButtonProps {
	size: keyof typeof sizes;
}

export function Button({ size }: ButtonProps)
	return <button css={{ ...sizes[size] }} />
}

TYPE SAFETY vs ARCHITECTURE

VANILLA EXTRACT

SPIRITUAL SUCCESSOR TO CSS MODULES

  • example.module.css
  • example.module.scss
  • example.css.ts

TYPESCRIPT — as our — CSS PREPROCESSOR

// example.css.ts
import { style } from '@vanilla-extract/css';

export const className = style({ color: 'blue'});

WHAT DOES THIS COMPILE TO?

// Virtual files
- example.vanilla.js
— example.vanilla.css
/* — Generated CSS */

.t402gs0 { color: blue }
// Example.tsx
import * as styles from './example.css.ts';

export default () => (
	<div className={styles.className}>... </div>
);

FRAMEWORK AGNOSTIC

  • — webpack
  • — Vite
  • — esbuild
  • — Rollup
  • — Parcel
  • — React
  • — Preact
  • — Vue
  • — Solid.js
  • — Svelte
  • — Qwik
  • — Astro
  • — Vanilla JS

GOES MUCH FURTHER THAN CSS MODULES

// Button.css.ts
import { style } from '@vanilla-extract/css';

export const color = {
	primary: style({ background: 'red' }),
	secondary: style({ background: 'pink' }),
};

export const size = {
	small: style({ padding: 10 }),
	large: style({ padding: 24 }),
};
// Button.tsx
import * as styles from './Button.css.ts';

interface ButtonProps {
	color: keyof typeof styles.color;
	size: keyof typeof styles.size;
}

export function Button({ color, size }: ButtonProps) {
return <button
className={[
styles.color[color],
styles.size[sizel,
].join(' ')} />
<Button color="oops" size="invalid">

STYLES AS DATA

// theme.css.ts
import { createVar } from '@vanilla-extract/css';

export const accentColorVar = createVar();

WHAT DOES THIS COMPILE TO?

// — Generated JS
export const accentColorVar = 'var(--t402gs0)';

/* — No generated CSS! (Yet) */

SETTING THE VARIABLE

// theme.css.ts
import { createVar, globalStyle } from '@vanilla-extract/css';

export const accentColorVar = createVar();

globalStyle(':root', {
	vars: {
		[accentColorVar]: 'blue'
	}
});

WHAT DOES THIS COMPILE TO?

// - Generated JS
export const accentColorVar = 'var (--t402gs0)':
// — Generated JS
export const accentColorVar = 'var(--t402gs0)';
/* — Generated CSS */ root { —-t402gs0: blue; }

ACCESSING THE VARIABLE

// Button.css.ts
import { style } from 'Qvanilla-extract/css';
import { accentColorVar } from '../theme.css.ts';

export const root = style({
	background: accentColorVar
});

TYPE-SAFE THEMING

// theme.css.ts import { createGlobalTheme } from '@vanilla-extract/css'; export const vars = createGlobalTheme(':root', { color: { primary: 'blue', secondary: ‘aqua', } space: { small: '8px', medium: '16px', large: '32px', }, });

// — Generated JS
export const vars = {
	color: {
		primary: 'var(--t402gs0)',
		secondary: 'var(--t402gs1)',
	},

	space: {
		small: 'var(--t402gs2)',
		medium: 'var(--t402¢s3)',
		Large: 'var(--t402gs4)',
	},
};
/* — Generated CSS */
:root {
—-t402gs0: blue;
—-t402gs1: aqua;
—-t402gs2: 8px;
—-t402gs3: 16px;
—-t402gs4: 32px;
}

MULTIPLE THEMES

ACCESSING THE THEME IN OUR COMPONENTS

// Button.css.ts
import { style } from 'Qvanilla-extract/css';

import { vars } from '../theme.css.ts';

export const root = style({
	background: vars.color.primary,
	padding: vars.space.medium,
});

UPDATING THE THEME AT RUNTIME

<Theme
colors={{
	primary: 'red',
	secondary: 'pink',
>}}
</Theme>
// Theme.tsx
import { assignInlineVars } from '@vanilla-extract/dynamic';
import { vars } from '../theme.css.ts';

interface ThemeProps {
	colors: Record<keyof typeof vars.color, string>;
}

export const Theme = ({ colors }: ThemeProps) => (
	<div style={assigniInlineVars(vars.color, colors) }>
	…
</div>
);

YOUR THEME IS DATA

// Button.css.ts
import { mapValues } from 'lodash';
import { vars } from '../theme.css.ts';

export const backgroundClasses = mapValues(vars.color, color => {
return style({ background: color });
});
// — Generated JS

export const backgroundColors = {
	primary: 'j4ia5g0',
	secondary: 'j4ia5g1',
	tertiary: 'j4ia5¢2',
};
/* — Generated CSS x/

.341a580 { background: var(--t402gs0); }
.341a581 { background: var(--t402gs1); }
.341a582 { background: var(--t402gs2); }
import { styleVariants } from '@vanilla-extract/css';
import { vars } from '../theme.css.ts';

export const backgroundClasses = styleVariants(vars.color, color => {
return ({ background: color });
});
<div class="w-24 h-24 rounded-full mx-auto">
/* -> Generated CSS */
.j4ia5g0 { background: var (--t402gs) ; }
.jia5g1 { background: var (--t402gs1); }
.j4ia5g2 { background: var (--t402gs2); }
.j4ia5g3 { background: var (--t402gs3); }
.j4ia5g4 { background: var (--t402gs4); }
.j4ia5g5 { background: var (--t402gs5); }
/* etc */
import { utils } from '../core.css.
<div className={[
	utils.display.block,
	utils.background.primary,
	utils.padding.medium.mobile,
	utils.padding. large.desktop,
].join(' ')}>

SPRINKLES

<Box
	display="block"
	background="primary"
	padding={{
		mobile: 'medium',
		desktop: 'large',
	}}>
<div class="j4ia5g3 j41a588 j4ia5gc j4ia58e ...">

TYPE-SAFE PRIMITIVES — for — DESIGN SYSTEMS

STATIC CSS & PREPROCESSORS

  • — Unified language
  • — Styles as data
  • — Type-safety
  • — Ship real CSS

MAINTAINABLE CSS

MAKE STATIC CSS THE SOURCE OF TRUTH