11/2022

Vanilla-Extract forall

Vanilla-Extract is a CSS preprocessor that allows us to write stylesheets in TypeScript and generate static CSS files during build time.

In this note we will look into creating a very simple set of tools and themes to be used by several frontend libraries/languages.

vanilla-extract-forall.jpg

Oh, nothing, Tommy. It's tiptop. It's just I'm not sure about the colour.

-- Turkish, Snatch

Cascading Style Sheets

CSS is a style sheet language that allows us to create styles for our websites. Just like the one we're looking at right now.

The CSS syntax consists on basic "property-value" pair (aka: declaration) and together with other declarations we can create blocks in a .css file or in a <style> tag and then apply the styles to one or multiple HTML elements.

The value definition syntax of a CSS property can be quite a special thing as it can differ a lot from property to property and the only way to check its correctness is to try it on a webpage.

.box {
border: 2px solid;
background-color: #FFF;
color: #1F1926;
font-size: 1.05rem;
padding: 10px 15px;
margin-top: 15px;
margin-bottom: 15px;
}
<div class="box">
Example goes here.
</div>

CSS-in-JS

For many years, we have been using tools like Sass, PostCSS, Less and others as preprocessors to create styles in a more reusable and scalable way. These amazing tools gave us great powers and abilities to create design systems to be used and re-used across multiple applications.

For the times they are a-changin'

CSS-in-JS is a way of writing our styles in JavaScript and similar to the CSS preprocessors it gave us amazing abilities to create reusable styles, in this case, using the same language and "next-to" the elements the styles apply. This approach gain big traction and popularity with the rise of "modern frameworks" like React.js, Vue.js and others.

const MyButton = styled.p`
color: pink;
padding: 10px;
`;
<MyButton>My button is pink!</MyButton>

We usually say that "with great power comes technical dept" and with CSS-in-JS came larger bundles of JavaScript that have to execute styles at runtime.

Zero-runtime Stylesheets in TypeScript

Vanilla-extract is "Zero-runtime Stylesheets in TypeScript", and that means we get all the benefits of a typed language and none of the runtime costs. Let's look at some API examples from vanilla-extract.

The style API allows us to create CSS declarations, just like in good old CSS with the advantage of being in TypeScript, the generated code will be a scoped CSS class that we can just apply to any HTML element.

The createTheme API allows us to create and extend themes that will generate locally scoped CSS variables to be used within the style API.

There's many more API provided by vanilla-extract, like styleVariants and createContainer, but we will keep our examples based on the style and createTheme.

const box = style({
border: '2px solid',
backgroundColor: '#FFF',
color: '#1F1926',
padding: '10px 15px'
})
<div class={box}>
Example goes here.
</div>
const [baseTheme, vars] = createTheme({
color: {
brand: '#000',
primary: '#000',
},
background: {
primary: '#FFF'
}
})
const themeReact = createTheme(vars, {
color: {
brand: '#61dafb',
primary: '#FFF'
},
background: {
primary: '#20232a'
}
})
const box = style({
backgroundColor: vars.background.primary,
color: vars.color.primary
})

Sprinkles & Recipes

Vanilla-extract also allows us to build CSS frameworks on top of it, some examples would be sprinkles and recipes.

With createSprinkles we can create a set of atomic properties to be consumed by any client application, for example:

export const sprinkles = createSprinkles(defineProperties({
defaultCondition: 'small',
conditions: {
small: {},
large: {'@media': '(min-width: 800px)'}
},
properties: {
color: themeVars.color,
backgroundColor: themeVars.background,
display: ['flex', 'grid'],
alignItems: ['center'],
paddingTop: themeVars.space,
paddingBottom: themeVars.space,
paddingLeft: themeVars.space,
paddingRight: themeVars.space,
},
shorthands: {
padding: ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight']
}
}))

Creates a sprinkles function that then can be used to apply a limited amount of properties to an element:

<div class={sprinkles({display: {small: 'flex', large: 'grid'}})}>
Example goes here.
</div>

We can also use the sprinkle utility together with recipe.

Recipe allows us to create a multi-variant utility function and use it to create specific variants for our elements:

export const box = recipe({
base: sprinkles({
paddingTop: 'large',
color: 'primary',
backgroundColor: 'primary'
}),
variants: {
border: {
some: sprinkles({border: 'some', borderRadius: 'some'}),
none: sprinkles({border: 'none'})
}
}
})

And then we can use it as:

<div class={box({border: 'some'})}>
Example goes here.
</div>

Use it without a framework

After we build a couple of themes, sprinkles and recipes, it comes the time to implement a small UI.

We first go with just HTML and build a very small example just like:

const body = document.querySelector<HTMLDivElement>('body')!
body.className = baseTheme
const app = document.createElement('div')
app.innerText = 'Example'
app.className = box({type: 'center', border: 'some'})
body.appendChild(app)

To implement this example we just need a build tool to integrate with vanilla-extract, we can use @vanilla-extract/vite-plugin for example.

Use it with any framework

We can also use vanilla-extract with any other framework (React, Vue, Svelte, etc), we just need to make sure to configure the build tool so we can use vanilla-extract.

An example with Svelte would be:

<script lang="ts">
import {box, text, themeSvelte} from 'vanilla'
</script>
<main class={`${themeSvelte} ${box({type: 'center', border: 'some'})}`}>
<h2 class={text({type: 'brand'})}>Svelte</h2>
</main>

Just like CSS

Being able to create a design system with multiple themes, utilities and variants that can be used across multiple clients without the overhead of being dependent to a specific UI framework, feels like we're just writing CSS.

Plus, we get all the advantages of using TypeScript to make sure all out properties and values are validated at build time.

You're still here?

Thank you so much for reading this! Go ahead and check the source code of some examples on GitHub with a few frontend frameworks.

You can also see the examples in action in the output website.

Until then!