I have an alias for the main css file in my vite.config.js
resolve: {
alias: {
"@main-css": path.resolve(__dirname, "./client/main.css"),
},
},
then in my vue components I can use that alias with @import
, but not with tailwind's @reference directive
<style lang="postcss" scoped>
// this works
@import '@main-css';
// this doesn't
@reference '@main-css';
</style>
This is the expected result. The @import
is processed by Vite, which is why it can resolve Vite-specific aliases. The @reference
is a TailwindCSS-specific directive and has nothing to do with Vite - so Vite doesn't know it should replace @main-css
with the correct path, and therefore it doesn't.
Although the import example works, don't use it with TailwindCSS in CSS modules as it will cause performance issues. Initialize Tailwind only once per project, then use @reference
in your modules to refer to it.
A quick fix is to avoid using aliases with @reference
.
<style lang="postcss" scoped>
@reference './../../client/main.css';
</style>
Are you sure you really need @reference
? The TailwindCSS team strongly advises against excessive use of @apply
, as it can be unhealthy for maintainability and hurt performance.
@apply
- StackOverflowYou can use CSS variables instead, or declare your styles in global.css
- it's not always necessary to define them at the scoped level.
You can add a Vite plugin that injects the @reference
directive into all your Vue files.
Note: This is also not recommended if not all Vue files need it, as it can lead to performance degradation.
Warning! I haven't verified the correctness of the code, so use it at your own risk. I do not recommend it for production projects.
./src/vite/TailwindAutoReference.ts
import { resolve } from "node:path"
import { createFilter, FilterPattern, ResolvedConfig, Plugin } from "vite"
/**
* A callback that determines whether a file should be skipped by the tailwindAutoReference plugin.
*
* @interface SkipFn
* @param {string?} code - The content of the file being transformed.
* @param {string?} id - The unique identifier of the file being transformed.
* @returns {boolean} - Returns true if the file should be skipped, false otherwise.
*/
interface SkipFn {
(code?: string, id?: string): boolean
}
/**
* A callback that determines which "@reference" should be added by the tailwindAutoReference plugin.
*
* @interface CssFileFn
* @param {string} code - The content of the file being transformed.
* @param {string} id - The unique identifier of the file being transformed.
* @returns {Promise<string|Array<string>>|string|Array<string>} - Returns the path to the Tailwind CSS file or an array of them.
*/
interface CssFileFn {
(code?: string, id?: string): string | string[] | Promise<string | string[]>
}
/**
* An options object for the tailwindAutoReference plugin.
*
* @interface PluginOption
* @property {Array<RegExp|string>} [include=[/\.vue\?.*type=style/]] - A list of picomatch patterns that match files to be transformed.
* @property {Array<RegExp|string>} [exclude=[]] - A list of picomatch patterns that match files to be excluded from transformation.
* @property {SkipFn} [skip=() => false] - A function that determines whether a file should be skipped. It takes the code and id as arguments and returns a boolean.
**/
interface PluginOption {
include?: FilterPattern,
exclude?: FilterPattern,
skip: SkipFn,
}
const defaultOpts: PluginOption = {
include: [/\.vue\?.*type=style/],
exclude: [],
skip: () => false
}
const resolveFn = (fn: unknown, ...args: unknown[]) =>
Promise.resolve(fn instanceof Function ? fn(args) : fn)
/**
* A Vite plugin that automatically adds "@reference" directives to Vue component style blocks.
*
* @function tailwindAutoReference
* @param {string|string[]|CssFileFn} [cssFile="./src/index.css"] - The path to the Tailwind CSS file or an array of them or a sync or async function that returns it or them.
* @param {Object} [opts] - An options object.
* @param {Array<RegExp|string>} [opts.include=[/\.vue/]] - A list of picomatch patterns that match files to be transformed.
* @param {Array<RegExp|string>} [opts.exclude=[]] - A list of picomatch patterns that match files to be excluded from transformation.
* @param {SkipFn} [opts.skip] - A function that determines whether a file should be skipped. It takes the code and id as arguments and returns a boolean.
* @returns {Object} - The plugin configuration object for Vite.
*/
const tailwindAutoReference = (
cssFile: string | string[] | CssFileFn = "./src/index.css",
opts = defaultOpts
): Plugin => {
const { include, exclude, skip } = { ...defaultOpts, ...opts }
let root: string, fileFilter: (id: string | unknown) => boolean
const getReferenceStr = (reference: string | string[]) =>
(Array.isArray(reference) ? reference : [reference]).reduce(
(acc, file) => `${acc}\n@reference "${resolve(root, file)}";`,
""
)
return {
name: "tailwind-auto-reference",
enforce: "pre",
configResolved: (config: ResolvedConfig) => {
root = config.root
fileFilter = createFilter(include, exclude, { resolve: root })
},
transform: async (code: string, id: string) => {
if (!fileFilter(id)) return null
if (!code.includes("@apply ") || skip(code, id)) return null
const lastUseMatch = [...code.matchAll(/^\s*@use.*\n/gm)].at(-1)
if (!lastUseMatch)
return {
code: `${getReferenceStr(await resolveFn(cssFile, code, id))}\n${code}`,
map: null
}
const before = code.substring(0, lastUseMatch.index)
const after = code.substring(lastUseMatch.index + lastUseMatch[0].length)
return {
code: `${before}${getReferenceStr(await resolveFn(cssFile, code, id))}\n${after}`,
map: null
}
}
}
}
export default tailwindAutoReference
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import TailwindAutoReference from './src/vite/TailwindAutoReference.ts'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
vue(),
tailwindAutoReference('./client/main.css'), // IMPORTANT: It must be registered before tailwindcss() official plugin.
tailwindcss(),
]
})
As I mentioned, adding @reference
to every Vue file is not the best idea - I wouldn't recommend it for a production project. However, you can write a Vite plugin that manually performs the alias replacement through your plugin.