We'll be talking about the example Tahua. This is a design system made with react, but you should be able to use it as insperation for any frontend technology.
Building
The main vite.config.ts
is for the build step, it's been set up to be as tree shakable as possible. This means that when we run the build each component will be its own file.
We'll use the button as our example for the output:
import "./../../src/styles/theme.css.ts.vanilla.css";
import "./../../src/components/Button/Button.css.ts.vanilla.css";
import { c as a } from "../../node_modules/@vanilla-extract/recipes/dist/createRuntimeFn-f8e161c6.esm.js";
var c = a({
defaultClassName: "d6ckgi0",
variantClassNames: {
color: { neutral: "d6ckgi1", brand: "d6ckgi2", accent: "d6ckgi3" },
size: { small: "d6ckgi4", medium: "d6ckgi5", large: "d6ckgi6" },
rounded: { true: "d6ckgi7" },
},
defaultVariants: { color: "accent", size: "medium" },
compoundVariants: [[{ color: "neutral", size: "large" }, "d6ckgi8"]],
});
export { c as button };
The main call out here is that due to this being split some build tools won't be happy that we're importing CSS inside each component. For example with Next.js and the pages router you'll need to transpile the module. The app router, vite, etc will work just fine.
You can remove the preserveModules
config if you want the build to just output one file which both imports all the css and contains all the JS.
We're using the preserveDirectives
plugin so that when we add "use client";
it stays inside the files (RSC is also one of the reasons to look at splitting your files)
For global styles, since they are import dependant, we have a styles.ts
file which imports the global styles. This should be loaded first in the consuming apps.
Storybook
Storybook support is fairly simple, our build config is made for making a library so we need to create a vite.config.ts
file which is using @vanilla-extract/vite-plugin
. We then update the main.ts
config to use our custom config.
framework: {
name: "@storybook/react-vite",
options: {
builder: {
viteConfigPath: ".storybook/vite.config.ts",
},
},
Testing
For running tests we're using vitest which has been fantastic, we just need to create a vitest config like so:
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vitest/config";
export default defineConfig({
plugins: [react(), vanillaExtractPlugin()],
test: {
globals: true,
include: ["src/**/*.test.{ts,tsx}"],
watch: false,
environment: "jsdom",
setupFiles: "src/tests/setup.ts",
},
});
The setup.ts
is fairly standard too:
import { afterEach } from "vitest";
import { cleanup } from "@testing-library/react";
import "@testing-library/jest-dom/vitest";
import "@vanilla-extract/css/disableRuntimeStyles";
// runs a cleanup after each test case (e.g. clearing jsdom)
afterEach(() => {
cleanup();
});
Using tokens outside the design system
If you're using tokens/theme variables you may run into issues importing them into .css.ts
files as there's no React runtime there. For that I'd recommend creating another export such as @org/design-system/css
.
Handling framework specific components
When using Next.js you're expected to use the Link
component from next/link
inplace of an a
tag. For this we just have a DesignSystemProvider
which gets passed a Link
component and an Image
component, but falls back on a
and img
when they aren't passed in.
Consuming
For Vite you'll need to:
- Create a
styles.ts
file and updateindex.html
to import it in the header - Update
styles.ts
to import the globalstyles.ts
export from the package.
This is just to handle the import order so that global styles are imported first, then classes afterwards.
For Next.js you'll need to
- Update either the root
layout.tsx
or_app.tsx
to include the globalstyles.ts
- You might need to update your
next.config.js
to transpile the design system (e.g. if you get errors for importing css inside components)
Fin
Hopefully this helped you get set up/understand more around making a desisn system with Vanilla Extract