Quite often, you’ll come across a line in documentation that says “alternatively, just do this…” that opens a whole tin of worms. In Tailwind’s documentation, we can find such a line in the Configuration: Referencing in JavaScript section.
In CSS files processed by PostCSS, we have the theme()
helper function, but in JavaScript, no such luck. So what do we do if we need to reference something in our Tailwind configuration? The documentation says on the subject:
To make this easy, Tailwind provides a
resolveConfig
helper you can use to generate a fully merged version of your configuration objectNote that this will transitively pull in a lot of our build-time dependencies, resulting in bigger client-side bundle size. To avoid this, we recommend using a tool like babel-plugin-preval to generate a static version of your configuration at build-time.
tailwindcss.com
If you’re using Webpack, the given advice likely works fine. For Vite, Babel is generally not used so would require adding it as a parser to your build process and likely gumming up the works. Enter: Vite Virtual Modules.
Virtual Modules
A virtual module is a Vite provision that allows us to provide an artificially constructed export to any scripts that is generated in both dev mode and builds. With these, we can import resources just like they came from the node_modules
folder on the filesystem.
For example, say you create a virtual module that exports the following:
export const message = "Hello World";
Inside our application, we can use it as simply as:
import { message } from "virtual:hello-world";
and because they’re compiled at build time, we can do some heavy-duty processing using the filesystem and not worry about bloating our frontend code.
How it works?
The process is simple enough. Everything that your application imports is processed by Vite to ensure that any loaders are applied (e.g. compiling SCSS down to CSS before it’s served to the browser) and other things. When importing a virtual module, Vite will need to do the following:
- Intercept the import request and modify the ID to ensure no other loaders process it
- When the time comes to “load” the resource, instead of loading a file, we return our artificially-generated content.
All of this can be coded inside a Vite plugin (a JavaScript object that contains functions to run on certain hooks and other configuration options. So here’s what our Hello World example would look like in the real world:
1// vite.config.js2import { defineConfig } from "vite";3
4const virtualModuleId = "virtual:hello-world";5const resolvedVirtualModuleId = "\0" + virtualModuleId;6
7export default defineConfig({8 // Your usual config here...9 plugins: [10 {11 name: "hello-world-module",12 resolveId(id) {13 if (id === virtualModuleId) {14 return resolvedVirtualModuleId;15 }16 },17 load(id) {18 if (id === resolvedVirtualModuleId) {19 return `export const message = "Hello World"`;20 }21 }22 }23 ]24});
Note that prepending “virtual:” to our module name is simply Vite convention. You can call it whatever you like.
Applying this to our Tailwind config
Instead of providing the Tailwind config functions to the browser, we’ll use them during our build process and then provide the resulting config to our application.
1// vite.config.js2import { defineConfig } from "vite";3import resolveConfig from "tailwindcss/resolveConfig";4import tailwindConfig from "./tailwind.config.js";5
6const fullConfig = resolveConfig(tailwindConfig);7
8const virtualModuleId = "virtual:tailwind-config";9const resolvedVirtualModuleId = "\0" + virtualModuleId;10
11export default defineConfig({12 // Your usual config here...13 plugins: [14 {15 name: "tailwind-config-module",16 resolveId(id) {17 if (id === virtualModuleId) {18 return resolvedVirtualModuleId;19 }20 },21 load(id) {22 if (id === resolvedVirtualModuleId) {23 return `export const config = ${JSON.stringify(fullConfig, null, 2)}`;24 }25 }26 }27 ]28});
Or if you only want to use your Tailwind colours inside your application, you might simply choose to export your theme’s colours instead of the entire resolved config object. Whatever you choose, accessing that config at runtime is now simply a case of
import { config } from "virtual:tailwind-config";
console.log(config);
Using virtual modules inside libraries
If you use Vite’s Library Mode to generate something using virtual modules, don’t forget to ensure the module ID is added to the build.rollupOptions.external
array as well as the optimizeDeps.exclude
array.