Control where your Vite app's CSS ends up — instead of <head>, styles are rendered exactly at the position of a <StylesTarget /> component in your React or Vue tree.
Vite normally injects all stylesheets into document.head. That breaks down when:
- your app renders inside a Shadow DOM (styles in
<head>don't reach it), - you build micro-frontends / widgets that must not leak styles into the host page,
- you simply need styles scoped to a specific part of the DOM.
This plugin intercepts Vite's CSS output and hands it to a <StylesTarget /> component that you place wherever the styles should live.
- The Vite plugin rewrites the build so each chunk registers its CSS (inlined string or file URL) in a global map and fires an event — nothing touches
<head>. - The
<StylesTarget />component (React or Vue) listens for that event and renders the registered styles at its own position — including inside a shadow root.
npm install vite-plugin-css-position1. Add the plugin to your vite.config.ts:
import { viteCssPosition } from "vite-plugin-css-position";
export default defineConfig({
plugins: [react(), /* or vue(), */ viteCssPosition()],
});2. Place <StylesTarget /> where the styles should be rendered:
| React | Vue |
|---|---|
import StylesTarget from "vite-plugin-css-position/react";
export function App() {
return (
<div>
<StylesTarget />
<span>Your App Content</span>
</div>
);
} |
<script setup lang="ts">
import StylesTarget from "vite-plugin-css-position/vue";
</script>
<template>
<div>
<StylesTarget />
</div>
</template> |
That's it — a production build now renders all stylesheets at the <StylesTarget /> position.
Note: By default the plugin only affects production builds. For dev-server/HMR support see Development mode.
The mode option controls how CSS is delivered. Rule of thumb:
- Just want it to work like v2? Use the default
"inject". - Large app with code splitting? Use
"injectPerChunk"— lazy components bring their CSS along only when loaded. - Want real, cacheable
.cssfiles (CSP, caching, lean JS)? Use"cssChunks".
"inject" (default) |
"injectPerChunk" |
"cssChunks" |
|
|---|---|---|---|
| CSS delivery | all CSS inlined into the entry JS | each chunk's CSS inlined into its JS | Vite's emitted .css files are kept |
| Rendered as | <style> |
<style> |
<link> or adoptedStyleSheets |
| Lazy-loading | no — all CSS up front | yes — per code-split chunk | yes — per code-split chunk |
Separate .css files (cacheable) |
no | no | yes |
| JS bundle size | largest | large | smallest |
viteCssPosition({ mode: "injectPerChunk" }); // or "cssChunks"The per-chunk modes require build.cssCodeSplit (Vite's default; forced on automatically).
In cssChunks mode the cssChunksStrategy option chooses how StylesTarget includes the CSS files:
"link"(default) — renders<link rel="stylesheet">. Simplest, but a<link>inside a Shadow DOM is not render-blocking, so a brief flash of unstyled content (FOUC) is possible while it loads."adopt"— fetches the CSS file and applies it viaadoptedStyleSheets. No FOUC, deduplicated across multiple shadow roots, and CSP-ideal. Requiresfetchand a modern browser (Chrome 73+ / Firefox 101+ / Safari 16.4+).
viteCssPosition({ mode: "cssChunks", cssChunksStrategy: "adopt" });viteCssPosition({
mode: "cssChunks",
cssChunksStrategy: "adopt",
enableDev: true,
});| Option | Type | Default | Description |
|---|---|---|---|
mode |
"inject" | "injectPerChunk" | "cssChunks" |
"inject" |
How CSS is delivered. See Choosing a mode. |
cssChunksStrategy |
"link" | "adopt" |
"link" |
Only with mode: "cssChunks": how CSS files are included. See link vs. adopt. |
enableDev |
boolean |
false |
Enable CSS handling in the dev server (HMR). See Development mode. |
instanceId |
string |
random | Identifier for this plugin instance. Set it when running multiple instances side by side to avoid conflicts. |
jsAssetsFilterFunction |
(chunk) => boolean |
entry chunks | Which JS output chunk(s) receive the CSS injection code. Useful with multiple entry points. |
mode only affects the production build. In the dev server, CSS is injected per module for HMR — but only if you opt in:
viteCssPosition({ enableDev: true });Without enableDev, the dev server behaves like plain Vite (styles in <head>).
No code changes required — the default mode: "inject" behaves exactly like 2.0.9.
What's new in 3.0.0:
- New
modeoption:"injectPerChunk"and"cssChunks"add component-level lazy-loading;"cssChunks"keeps Vite's emitted.cssfiles (see Choosing a mode). - Zero runtime dependencies — the CSS-by-JS injection is now built in.
See the CHANGELOG for details.
- Vite 5, 6, or 7
- Node.js ≥ 20.12
- React 18/19 or Vue 3 (for the bundled
StylesTargetcomponents)
# Install dependencies
pnpm install
# Run the playground
pnpm run play
# Run the tests
pnpm test
# Build the library
pnpm run buildThe built-in CSS-by-JS injection is a trimmed, vendored port of
vite-plugin-css-injected-by-js
by Marco Prontera (MIT License).
MIT © Alexander Bogoslawski