Vite plugin to inline CSS Modules for dynamically imported code.
npm install vite-plugin-lazy-css-modules-inlinerNote: A Russian version of this README is available below.
A Vite plugin that enables true on-demand CSS for dynamically imported code by virtualizing CSS Modules. It prevents lazy components' styles from being bundled into the page CSS and injects them only when the corresponding JS is actually used.
npm i vite-plugin-lazy-css-modules-inliner --save-dev
Example of usages see in section Examples.
- Vite (vanilla)
- CSS-modules
- Astro + Vite
- React / Vue / Svelte — when styles are imported as external *.module.css
Limitations: native SFC styles ( inside .svelte/.vue) are out of scope.
- Keep page (synchronous) CSS clean from lazy modules' styles
- Load lazy JS + CSS together on demand
- Preserve CSS Modules hashing consistency between SSR and client
- Avoid preloading page CSS/JS from __vitePreload in dynamic chunks
1. Detect dynamic roots
- SSR: resolveDynamicImport marks import() targets as roots
- Client: transform uses getModuleInfo(id).dynamicImporters and also promotes dynamicallyImportedIds as roots
2. Propagate laziness
- resolveId treats children of lazy importers as part of the lazy graph
- CSS imports inside the lazy graph are rerouted to virtual JS modules
3. Virtual CSS modules
- SSR load: returns an empty module — styles are not collected into the page CSS bundle
- Client load:
- reads CSS, applies postcss-modules if needed, minifies with cssnano in prod
- generates a virtual JS module that imports a shared runtime (lazy-css-inliner:runtime)
and either:
- plain CSS: calls ensureLazyCssInjected(id, css) immediately
- CSS Modules: exports a Proxy that injects CSS only on the first token access
4. Strip CSS deps from __vitePreload
- renderChunk filters out .css entries when stripPreloadDepsMode: 'css' (or all deps when 'all')
Note about bundling: virtual CSS code can still end up inside a shared parent chunk depending on Rollup splitting. This does not inject styles early — injection happens only when the virtual module executes (or when CSS‑Module tokens are accessed). If you want stricter chunk boundaries, add simple manualChunks rules in your Vite config.
``ts
export type StripPreloadDepsMode = 'all' | 'css';
export interface PluginOptions {
stripPreloadDepsMode?: StripPreloadDepsMode; // default: 'css'
isDev?: boolean; // dev switch (sourcemaps/minify)
includedPathes?: string[]; // roots to include; default: [path.join(root,'src')]
excludedPathes?: string[]; // paths to exclude; default: ['node_modules']
runtimeIsRtlCondition?: string; // optional JS condition evaluated at runtime to enable RTL (e.g. 'window.isRtl')
}
`
Defaults are applied at configResolved when not provided.
If runtimeIsRtlCondition is provided, the plugin enables RTL mode:
- Duplicates client chunks under rtl/ and keeps all imports relative so nested dynamic imports resolve within rtl/.rtlcss
- Converts inlined CSS to RTL during duplication using lib..rtl
- Appends to lazy CSS ids to avoid collisions.__vitePreload
- Filters deps at runtime so only the appropriate (LTR/RTL) JS and CSS are requested.
- Mixed static + dynamic imports of the same module can duplicate CSS by design. Prefer picking one strategy or accept the trade‑off.
- Above‑the‑fold dynamic modules will inject CSS very early — consider static import instead.
- If virtual CSS appears in a shared parent chunk, consider build.rollupOptions.output.manualChunks to separate domains (dialogs, overlays, etc.).
- vite.config.ts
`ts
import { defineConfig } from 'vite';
import { viteLazyCssInliner } from 'vite-plugin-lazy-css-modules-inliner';
import path from 'node:path';
export default defineConfig({
plugins: [
viteLazyCssInliner({
includedPathes: [path.join(process.cwd(), 'src')],
excludedPathes: ['node_modules'],
stripPreloadDepsMode: 'css', // or 'all' to disable all __vitePreload deps
isDev: process.env.NODE_ENV === 'development',
runtimeIsRtlCondition: 'window.isRtl',
}),
],
});
`
---
Плагин Vite, виртуализирующий CSS Modules для динамически импортируемого кода. Стили ленивых модулей не попадают в общий CSS страницы и инжектятся только при фактическом использовании соответствующего кода.
npm i vite-plugin-lazy-css-modules-inliner --save-dev
Примеры использования см. в секции Examples.
- Vite (vanilla)
- Astro + Vite
- React / Vue / Svelte — при подключении *.module.css
Ограничения: встроенные стили SFC (