DEV Community

Cover image for Anatomy of a package: @vanilla-extract/css
Ziyad
Ziyad

Posted on

Anatomy of a package: @vanilla-extract/css

Exploring and reading code from an open-source project is one of the best things you can do to learn and grow. Initially, you will not understand what's happening, you may question why a certain thing is done in a certain way, but eventually you will start understanding the flow, you'll start to grasp the motivation of the author and once you do, it'll unlock a new perspective on writing code that you can very well use in your own code.

In this post, take a deep dive with me in one such open-source project - @vanilla-extract/css. We'll take a look at why it exists, what problem it solves and crazy witchcraft in its code.

What is it?

From its documentation -

Basically, it’s “CSS Modules-in-TypeScript” but with scoped CSS Variables + heaps more.

Vanilla-extract is co-created with the developers of CSS Modules, so you'll find many similarities with it and according to me it's an improvement over CSS Modules.

Let's quickly look at how it works

Create *.css.ts, *.css.tsx, *.css.js, *.css.jsx file and create your "css class" like shown below
styles.css.ts

Then in your component file, import and use it like this
AboutSection.tsx

After build, this will be converted to this -

From chrome devtools

From chrome devtools

The style() returns a unique string that becomes the class name, and its corresponding styles are generated in a .css file that will be included in your final build.

It is capable of doing way more than generating classes; you can build fully typed design systems with excellent theming, you can generate variants with sprinkles or use its utility classes for dynamic calculation of values from JS to CSS. Check out their documentation to learn more.

Now, vanilla-extract is framework agnostic, but it requires a build tool to process and generate css files from typescript or javascript files.

In the following sections, we will explore how does it do what it does using one such tool called esbuild

Code structure

vanilla-extract github repository - https://github.com/vanilla-extract-css/vanilla-extract

When you open the above repository, you'll notice that it's a Monorepo, more specifically a pnpm workspace

What is a monorepo, and what is a pnpm workspace is out of scope of this post, but in 2025 it's essential to know, you'll find most modern open-source projects are using them.

It's a big repository, but don't get overwhelmed; you don't need to understand all of it. We will just be focusing on the following directories:

  • packages/css
  • packages/esbuild-plugin/src
  • packages/integration/src

packages/css

This package contains core business logic. Every other package, including all the plugins for build tools like esbuild, vite, webpack etc. depends on this package.

It exports all core functions and utilities that are imported from @vanilla-extract/css package

dist/vanilla-extract-css.esm.js

I cannot possibly cover each and every one of them, but I will try and explore the flow of style() function and explain it with the best of my ability.

Before getting to style() we need to understand a very crucial file - adapter.ts

Following piece of code is responsible for collecting class names, css rules inside style() functions and more using an adapter. An adapter in this case is simply an object of functions

export const mockAdapter: Adapter = {
  appendCss: () => {},
  registerClassName: () => {},
  onEndFileScope: () => {},
  registerComposition: () => {},
  markCompositionUsed: () => {},
  getIdentOption: () =>
    process.env.NODE_ENV === 'production' ? 'short' : 'debug',
};
Enter fullscreen mode Exit fullscreen mode

This file maintains a module scoped constant variable called adapterStack which is just an array of Adapter.
You can add new adapter to stack using exported function setAdapter() and get last adapter in stack using currentAdapter().

This is functional programming and EcmaScript Modules at its best, this would have easily been a class but classes are not first class citizen in Javascript and using es-modules or esm make code more readable and maintainable to a certain point as there is no need to handle and track scope of infamous this (iykyk)

adapterStack is not exported from this file so it can't be modified directly outside the scope of this file, so if you need to manipulate it in anyway you have to use functions exported from this file, this behaviour should be similar to lot of you guys as I just described a "private" member of this cla... hmm ... I mean module.

Now let's move forward to our next file style.ts

At line:70 during the time of writing, you can find the definition of style()

export function style(rule: ComplexStyleRule, debugId?: string): string {
  if (Array.isArray(rule)) {
    return composedStyle(rule, debugId);
  }

  const className = generateIdentifier(debugId);

  registerClassName(className, getFileScope());
  appendCss({ type: 'local', selector: className, rule }, getFileScope());

  return className;
}
Enter fullscreen mode Exit fullscreen mode

As you can see it returns a string, wonder what might this be 🤔, you guessed it, it's a css class name, the variable name gave it away.

But let's focus on two function calls after that registerClassName() and appendCss(). Both of these functions are exported from ./adapter.ts and internally calls the respective functions from the last adapter added in the stack.

The last file from this package that we will be looking at is - transformCss.ts
This is the central part of this whole package. It contains business logic on how to convert Javascript objects to actual CSS. It takes in the object generated by appendCss() containing class name and rule which is the object we pass to style() function in our *.css.* files. It returns an array of string which eventually become the content of generated CSS files in your final build output.

I let it upto you to explore and understand the actual code behind converting JS to CSS, it's mostly walking tree with string manipulation (to put it shamefully, underminingly and extremely simply)

packages/esbuild-plugin/src

After reading the previous section, you must wonder that in order to collect class names and rule objects we must run the functions in *.css.* files which must call the functions in adapter and then eventually transformCss(). But I've always thought that build tools like esbuild, vite etc. only statically analyze and transform code to be used in production, right?

Let's dive in and figure out what's happening, shall we

There's only one file in this directory - index.ts

It exports a plugin that can be used with ... (🥁) ... esbuild.

To check how this plugin is used with esbuild you can quickly take a look at this file esbuild.ts. We will come back to this later

This is very standard stuff, it basically add two more flows to esbuild build pipeline, let's try and understand how they transform source code

First, this functions is called for every file that matches this filter /\.css\.(js|cjs|mjs|jsx|ts|tsx)(\?used)?$/ which are the files that contain our vanilla-extract code.
It seems that this is calling two functions coming from @vanilla-extract/integration package, compile() and processVanillaFile(), we will look at both of them later but for now you have to trust me on this, content returned from processVanillaFile() is a string which when returned from onLoad becomes the content of the file, so initially this file contained our code that we wrote and then it is replaced by whatever content is now

 build.onLoad({ filter: cssFileFilter }, async ({ path }) => {
        const combinedEsbuildOptions = { ...esbuildOptions };
        const identOption =
          identifiers ?? (build.initialOptions.minify ? 'short' : 'debug');

        // To avoid a breaking change this combines the `external` option from
        // esbuildOptions with the pre-existing externals option.
        if (externals) {
          if (combinedEsbuildOptions.external) {
            combinedEsbuildOptions.external.push(...externals);
          } else {
            combinedEsbuildOptions.external = externals;
          }
        }

        const { source, watchFiles } = await compile({
          filePath: path,
          cwd: build.initialOptions.absWorkingDir,
          esbuildOptions: combinedEsbuildOptions,
          identOption,
        });

        const contents = await processVanillaFile({
          source,
          filePath: path,
          outputCss,
          identOption,
        });

        return {
          contents,
          loader: 'js',
          watchFiles,
        };
      });
Enter fullscreen mode Exit fullscreen mode

Second onLoad is with filter that matches all files but with namespace vanillaCssNamespace

Note: onResolve function in this plugin set namespace on certain files

It seems that it's extracting content of a virtual file and returning it as a file. return object is telling esbuild that a file whose name is fileName and contains content source is a css file. We can safely assume that this source is the generated css that will eventually end up in build files.

 build.onLoad(
        { filter: /.*/, namespace: vanillaCssNamespace },
        async ({ path }) => {
          let { source, fileName } = await getSourceFromVirtualCssFile(path);

          if (typeof processCss === 'function') {
            source = await processCss(source);
          }

          const rootDir = build.initialOptions.absWorkingDir ?? process.cwd();

          const resolveDir = dirname(join(rootDir, fileName));

          return {
            contents: source,
            loader: 'css',
            resolveDir,
          };
        },
      );
Enter fullscreen mode Exit fullscreen mode

packages/integration/src

Now the meaty part, this is what blew my mind and encouraged me to write this article

We saw two functions compile() and processVanillaFile().

First function is coming from compile.ts

Now if you look closely we have only created onLoad function, which will only run when esbuild loads the file in memory but it will not transform it yet. It's using esbuild again to extract a node-targeted code that can be run using eval() (More on this later), but aren't we already using esbuild? Confused? Worry not it's not that complicated.

compile() internally calls another instance of esbuild() which has yet another plugin called vanillaExtractTransformPlugin() and the output of this process is what we store as the content of a js/ts file in the outside plugin.

So basically this is the call execution stack

user esbuild config -> 
onLoad in vanillaExtractPlugin() -> 
internal esbuild config -> 
onLoad in vanillaExtractTransformPlugin()
Enter fullscreen mode Exit fullscreen mode

Since we are only passing one file, onLoad inside vanillaExtractTransformPlugin() only run once and transform code inside this single file, we return this transformed code and pass it to processVanillaFile() as source

const result = await esbuild({
    entryPoints: [filePath],
    metafile: true,
    bundle: true,
    external: ['@vanilla-extract', ...(esbuildOptions?.external ?? [])],
    platform: 'node',
    write: false,
    plugins: [
      vanillaExtractTransformPlugin({ identOption }),
      ...(esbuildOptions?.plugins ?? []),
    ],
    absWorkingDir: cwd,
    loader: esbuildOptions?.loader,
    define: esbuildOptions?.define,
    tsconfig: esbuildOptions?.tsconfig,
    conditions: esbuildOptions?.conditions,
  });
Enter fullscreen mode Exit fullscreen mode

Moving on, vanillaExtractTransformPlugin() calls transform() on the file content coming from transform.ts which internally calls addFileScope() coming from addFileScope.ts which literally just add few lines of code at top and bottom of source code

source = dedent(`
        import { setFileScope, endFileScope } from "@vanilla-extract/css/fileScope";
        setFileScope("${normalizedPath}", "${packageName}");
        ${source}
        endFileScope();
      `);
Enter fullscreen mode Exit fullscreen mode

So after running compile() our source code content has been transformed and got additional few lines of code at top and bottom of file.

This transformed source code is passed to processVanillaFile() which is the last piece of puzzle in our journey.

porcessVanillaFile.ts

Remember our little "Adapter", well here it is, let's bask in its glory!

  const cssByFileScope = new Map<string, Array<Css>>();
  const localClassNames = new Set<string>();
  const composedClassLists: Array<Composition> = [];
  const usedCompositions = new Set<string>();

  const cssAdapter: Adapter = {
    appendCss: (css, fileScope) => {
      if (outputCss) {
        const serialisedFileScope = stringifyFileScope(fileScope);
        const fileScopeCss = cssByFileScope.get(serialisedFileScope) ?? [];

        fileScopeCss.push(css);

        cssByFileScope.set(serialisedFileScope, fileScopeCss);
      }
    },
    registerClassName: (className) => {
      localClassNames.add(className);
    },
    registerComposition: (composedClassList) => {
      composedClassLists.push(composedClassList);
    },
    markCompositionUsed: (identifier) => {
      usedCompositions.add(identifier);
    },
    onEndFileScope: () => {},
    getIdentOption: () => identOption,
  };
Enter fullscreen mode Exit fullscreen mode

As you can see, we have recreated our adapter and each function in adapter is populating four variables, we only need to focus on these two map cssByFileScope and set localClassNames.

After this we append a line to our source string

 const adapterBoundSource = `
    require('@vanilla-extract/css/adapter').setAdapter(__adapter__);
    ${source}
  `;
Enter fullscreen mode Exit fullscreen mode

Now our source string have code from our original file with few lines added at top and bottom responsible for doing these

  • import and call setAdapter()
  • import and call setFileScope(), endFileScope()

Remember, this is just string manipulation till this point, we took the code from our *.css.* files, extracted and transformed it using esbuild and added few lines of code to the content by means of string manipulation.

Now we take this string which contains our code and run this code using eval

const evalResult = evalCode(
    adapterBoundSource,
    filePath,
    { console, process, __adapter__: cssAdapter },
    true,
  ) as Record<string, unknown>;
Enter fullscreen mode Exit fullscreen mode

In third argument we are passing our precious little adapter to replace a string at __adapter__ in source code which is precisely placed at an argument of setAdapter(), and do you, yes you 🫵 REMEMBER what does our source code contains? You are goddamn right! style() functions, which internally calls adapter functions which we have just passed in context which means as soon as this code runs our appendCss() and registerClassName() from adapter will run and we will get the values of localClassNames and cssByFileScope

mind=blown

This eval() is why we "compiled" our code platform: 'node' using another esbuild instance inside ... esbuild instance.

We pass this collected cssByFileScope to transformCss() from package/css which returns, well, CSS!

Just bear with me, we are almost at the finish line

After this few more things happen in processVanillaFile() function -

  1. generated css string is zipped into a base64 string using zlib library
  2. Zipped string is added as a ?source param and appended to an import statement

So our *.css.* files after going through compile() and processVanillaFile() looks like this

import 'src/styles.css.ts.vanilla.css?source=LnN0eWxlc19jb250YWluZXJfXzFhbXY1bW8yIHsKICBjb250YWluZXItdHlwZTogc2l6ZTsKICBjb250YWluZXItbmFtZTogc3R5bGVzX215LWNvbnRhaW5lcl9fMWFtdjVtbzE7CiAgd2lkdGg6IDUwMHB4Owp9Ci5zdHls
ZXNfYmxvY2tfXzFhbXY1bW8zIHsKICAtLWNvbG9yX18xYW12NW1vMDogYmx1ZTsKICBiYWNrZ3JvdW5kLWNvbG9yOiB2YXIoLS1jb2xvcl9fMWFtdjVtbzApOwogIHBhZGRpbmc6IDIwcHg7Cn0KQG1lZGlhIHNjcmVlbiBhbmQgKG1pbi13aWR0aDogMjAwcHgpIHsKICBAY29udGFpb
mVyIHN0eWxlc19teS1jb250YWluZXJfXzFhbXY1bW8xIChtaW4td2lkdGg6IDQwMHB4KSB7CiAgICAuc3R5bGVzX2Jsb2NrX18xYW12NW1vMyB7CiAgICAgIGNvbG9yOiB3aGl0ZTsKICAgIH0KICB9Cn0=';
Enter fullscreen mode Exit fullscreen mode

When esbuild encounters this import statement it looks for a file called src/styles.css.ts.vanilla.css which ... doesn't exist? Oh wait, we added two onLoad and one of them is for virtual files whose name ends with .vanilla.css, what a coincidence?

This second onLoad calls getSourceFromVirtualCssFile() which as I said before returns unzipped string containing actual css, which we added as ?source param in import statement, and it comes full circle this is how our css in a virtual file becomes a real css file. Simply Genius!

I intentionally left out code related to fileScope, it's nothing but an object containing filePath and packageName. For a given fileScope style() always generate same hash, so as long as you are not changing your file location it's class names will always generate same hash across builds.

Summary

Each step which we discussed happens for every single *.css.* file and using onLoad event we convert them to css files, now esbuild can consume these as normal css files and insert them into build directory or process them further based on it's configuration.

We explored one plugin for esbuild tool, but there are plugins for other build tools like vite, webpack, astro etc. Internally all of them follow somewhat similar logic but every tool is different and requires different integration.

Hope you have enjoyed this article and it encourages you to start exploring source code of your favorite libraries and if opportunity arises even contribute to it.

Thank you!

Top comments (0)