Zero-runtime scoped CSS, targeting any framework on any bundler.
A simple example:
import { css } from 'unplugin-inline-css-modules'
const classes = css`
.button {
background-color: #1f1e33;
color: white;
padding: 1rem 2rem;
border-radius: 4px;
}
`
export const Button = () => <button className={classes.button}>Click me</button>At build time, the CSS is extracted into a real CSS module. Existing PostCSS syntax, Tailwind @apply directives, and CSS tooling all work out of the box however they were configured in the bundler.
Frameworks like Vue have <style scoped>. The rest of us have been stuck choosing between separate .module.css files (context-switching) and CSS-in-JS libraries (runtime overhead, incompatible with PostCSS tooling).
This plugin gives you the best of both worlds: co-located styles with zero runtime cost. Under the hood it just generates CSS modules, so your entire CSS toolchain works without any changes.
npm install unplugin-inline-css-modulesAdd the plugin for your bundler:
Vite
// vite.config.ts
import { defineConfig } from 'vite'
import inlineCSSModules from 'unplugin-inline-css-modules/vite'
export default defineConfig({
plugins: [inlineCSSModules()],
})Rollup
// rollup.config.js
import inlineCSSModules from 'unplugin-inline-css-modules/rollup'
export default {
plugins: [inlineCSSModules()],
}Rolldown
// rolldown.config.js
import inlineCSSModules from 'unplugin-inline-css-modules/rolldown'
export default {
plugins: [inlineCSSModules()],
}Webpack
// webpack.config.js
import inlineCSSModules from 'unplugin-inline-css-modules/webpack'
export default {
plugins: [inlineCSSModules()],
}Rspack / Rsbuild
// rsbuild.config.ts
import { defineConfig } from '@rsbuild/core'
import inlineCSSModules from 'unplugin-inline-css-modules/rspack'
export default defineConfig({
tools: {
rspack: {
plugins: [inlineCSSModules()],
},
},
})esbuild
import { build } from 'esbuild'
import inlineCSSModules from 'unplugin-inline-css-modules/esbuild'
build({
plugins: [inlineCSSModules()],
})Farm
Farm is an experimental target. There has not been thorough testing for functionality.
// farm.config.ts
import inlineCSSModules from 'unplugin-inline-css-modules/farm'
export default {
vitePlugins: [inlineCSSModules()],
}Bun
import inlineCSSModules from 'unplugin-inline-css-modules/bun'
Bun.build({
plugins: [inlineCSSModules()],
})Next.js
// next.config.ts
import type { NextConfig } from 'next'
import inlineCSSModules from 'unplugin-inline-css-modules/next'
const nextConfig: NextConfig = {
webpack: config => {
config.plugins = config.plugins || []
config.plugins.push(inlineCSSModules())
return config
},
}
export default nextConfigNote: SWC breaks some assumptions for virtual module resolution. As a workaround, CSS modules are cached in
node_modules/.cache/inline-css-modules/. If you reinstallnode_modules, remove the.nextfolder to clear stale references.
Nuxt
// nuxt.config.ts
import inlineCSSModules from 'unplugin-inline-css-modules/vite'
export default defineNuxtConfig({
vite: {
plugins: [inlineCSSModules()],
},
})Astro
// astro.config.mjs
import { defineConfig } from 'astro/config'
import inlineCSSModules from 'unplugin-inline-css-modules/astro'
export default defineConfig({
integrations: [inlineCSSModules()],
})SvelteKit
// vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite'
import { defineConfig } from 'vite'
import inlineCSSModules from 'unplugin-inline-css-modules/vite'
export default defineConfig({
plugins: [inlineCSSModules(), sveltekit()],
})import { css } from 'unplugin-inline-css-modules'
const classes = css`
.root {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.button {
background-color: #1f1e33;
color: white;
padding: 1rem 2rem;
border: none;
border-radius: 4px;
cursor: pointer;
}
`
export const Root = () => (
<div className={classes.root}>
<button className={classes.button}>Click me</button>
</div>
)<script setup lang="ts">
import { css } from 'unplugin-inline-css-modules'
const classes = css`
.container {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.button {
background-color: #1f1e33;
color: white;
padding: 1rem 2rem;
border: none;
border-radius: 4px;
cursor: pointer;
}
`
</script>
<template>
<div :class="classes.container">
<button :class="classes.button">Click me</button>
</div>
</template>import type { Component } from 'solid-js'
import { css } from 'unplugin-inline-css-modules'
const styles = css`
.container {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.button {
background-color: #1f1e33;
color: white;
padding: 1rem 2rem;
border: none;
border-radius: 4px;
cursor: pointer;
}
`
const App: Component = () => (
<div class={styles.container}>
<button class={styles.button}>Click me</button>
</div>
)
export default App<script lang="ts">
import { css } from 'unplugin-inline-css-modules'
const classes = css`
.button {
background-color: #1f1e33;
color: white;
padding: 1rem 2rem;
border-radius: 4px;
}
`
</script>
<button class={classes.button}>Click me</button>---
import { css } from 'unplugin-inline-css-modules'
const classes = css`
.button {
background-color: #1f1e33;
color: white;
padding: 1rem 2rem;
border-radius: 4px;
}
`
---
<button class={classes.button}>Click me</button>At build time, the plugin transforms your css tagged template literals into real CSS module imports:
// What you write:
const classes = css`
.root {
color: red;
}
`
// What gets compiled:
import classes from 'virtual:inline-css-modules/App-0.module.css'The CSS gets extracted into a virtual module and then processed through your bundler's normal CSS pipeline. This means PostCSS plugins, Tailwind @apply, preprocessors, and any other CSS tooling work exactly as they would with a regular .module.css file.
| Option | Type | Default | Description |
|---|---|---|---|
tagName |
string |
'css' |
Template tag name to match. Useful for avoiding conflicts with other CSS-in-JS libraries. |
fileMatch |
RegExp |
/\.(tsx|jsx|js|vue|svelte)$/ |
Pattern for files to transform. |
extension |
'css' | 'scss' | 'sass' | 'styl' | 'less' | (filename: string) => SupportedExtension |
'css' |
CSS preprocessor to use. Can be a string or a function that returns the extension based on the filename. |
inlineImport |
boolean |
true |
When false, generated imports are hoisted to the top of the file instead of replacing the declaration inline. |
No string interpolation. The css tag looks like a template literal, but it's a compile-time transform. The contents are moved into a real CSS module, so dynamic values can't work.
Class variables are replaced at compile time. The const classes = css\...`` declaration is replaced with an import statement, so you can't reassign or manipulate the variable at runtime.
css is not defined or similar errors -- Make sure the tagName option matches the tag you're using in your code. The plugin removes the import from unplugin-inline-css-modules and replaces the tagged template with a CSS module import. If the tag names don't match, the import gets removed but the template isn't transformed.