Vite 08 ⚡ CSS Handling — Plain CSS, CSS Modules & preprocessors
Vite treats CSS as a first-class module in the graph. Importing a .css file from JavaScript is not a side effect that gets ignored; it is a module import that Vite processes, injects during development, and extracts into a production stylesheet. The same pipeline handles plain CSS, CSS Modules (scoped by filename convention), and preprocessors like Sass or Less (scoped by file extension). The configuration surface is small because Vite detects the file type and applies the right transformer automatically.
This chapter covers the three paths: plain CSS imported for global styles, CSS Modules that generate scoped class names from the filename, and preprocessors that require only the dependency to be installed. It also covers the ?inline suffix that imports processed CSS as a string without injecting it, the PostCSS integration that runs automatically when a config file exists, and the CSS code-splitting behavior that loads styles only when the associated async chunk loads.
Key point: CSS Modules are activated by the filename, not by configuration. A file named Button.module.css becomes a module that returns a class-name map. A file named Button.css becomes global CSS that returns nothing.
Why Vite handles CSS this way
The global-scope problem. A plain CSS import injects the stylesheet globally. The selectors inside it apply to the entire page, regardless of which component imported the file. This is the expected behavior for a reset stylesheet or a design-system base, but it is the wrong behavior for a component-specific style that happens to use a class name like .button .
The scoping problem. CSS Modules solve the naming collision by rewriting class names into unique hashes at build time. Vite’s documentation describes the mechanism: the file must end in .module.css, and the default import returns an object mapping the original class name to the generated one . The component uses styles.button, and the actual class name in the DOM is something like Button_button__a1B2c. The stylesheet is still global in the sense that it is injected into the page, but the selectors are unique and cannot collide .
The preprocessor problem. Sass, Less, and Stylus need a compilation step. Vite does not bundle a preprocessor implementation; it detects the file extension and looks for the corresponding package in node_modules. If sass is installed, .scss and .sass files are compiled. If less is installed, .less files are compiled. No configuration is required beyond the dependency itself .
a. Plain CSS imports
A plain CSS import is the recommended way to apply global styles. Vite injects the CSS into the page during development via a <style> tag and extracts it into a production stylesheet at build time .
// main.js
import './styles/global.css'
The import returns nothing. The styles apply globally. This is the correct mechanism for a reset, a font-face declaration, a CSS custom property definition, or a base typography stylesheet.
If the processed CSS is needed as a string rather than injected, the ?inline suffix returns the compiled CSS without adding it to the page .
import cssString from './styles/foo.css?inline'
// cssString contains the processed CSS text.
// The styles are NOT injected.
This is useful for constructing a shadow DOM stylesheet or for passing CSS to a third-party library that expects a string.
b. CSS Modules
CSS Modules are enabled by the .module.css naming convention. The default import returns an object with the class-name mapping .
/* Button.module.css */
.button {
background-color: blue;
border-radius: 8px;
}
// Button.jsx
import styles from './Button.module.css'
export function Button() {
return <button className={styles.button}>Click me</button>
}
At runtime, styles.button is a unique string. The generated class name is deterministic but includes a hash to prevent collisions with other module files . The same .button class in another module file receives a different hash, so the two styles cannot override each other.
The same naming convention applies to preprocessor files. A file named Button.module.scss is both a CSS Module and a Sass file. Vite runs the preprocessor first, then applies the module scoping to the compiled CSS .
c. Preprocessors
Vite does not include Sass, Less, or Stylus by default. Installing the package is the entire configuration.
# For Sass
npm install -D sass
# For Less
npm install -D less
After installation, importing a .scss or .less file works without any changes to vite.config.js .
// App.jsx
import './styles/main.scss'
The file extension is the signal. Vite detects .scss and invokes the sass package. It detects .less and invokes the less package. If the package is not installed, the build fails with an error that names the missing dependency. This is deliberate: it keeps the core small and avoids bundling preprocessors that a project does not use.
For preprocessor options, the css.preprocessorOptions field in vite.config.js passes configuration to the compiler. A common pattern is injecting global variables into every file:
// vite.config.js
export default {
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "./src/variables.scss";`,
},
},
},
}
The additionalData string is prepended to every .scss file before compilation, which makes the variables available without an explicit import in each file .
Complete Example Session
// ============================================
// PART 1: PLAIN GLOBAL CSS
// ============================================
// main.js
import './styles/global.css'
// The styles are injected globally.
// The import returns nothing.
// ============================================
// PART 2: ?INLINE STRING IMPORT
// ============================================
import cssText from './styles/shadow.css?inline'
// cssText is the processed CSS string.
// The styles are NOT injected into the page.
/* ============================================
PART 3: CSS MODULE FILE
============================================ */
/* Button.module.css */
.button {
background: blue;
padding: 8px 16px;
}
// ============================================
// PART 4: CSS MODULE IMPORT
// ============================================
import styles from './Button.module.css'
export function Button() {
return <button className={styles.button}>Click</button>
}
// styles.button is a unique hashed class name.
// ============================================
// PART 5: CSS MODULE WITH TAILWIND UTILITIES
// ============================================
import styles from './Button.module.css'
export function Button() {
return <button className={`${styles.button} font-bold`}>Click</button>
}
// Module classes and global utilities can coexist.
# ============================================
# PART 6: INSTALL SASS
# ============================================
npm install -D sass
// ============================================
// PART 7: IMPORT A SCSS FILE
// ============================================
// App.jsx
import './styles/main.scss'
// Vite detects .scss, invokes sass, injects the result.
/* ============================================
PART 8: SCSS MODULE
============================================ */
/* Card.module.scss */
$primary: #3498db;
.card {
background: $primary;
padding: 16px;
}
// ============================================
// PART 9: SCSS MODULE IMPORT
// ============================================
import styles from './Card.module.scss'
// The preprocessor runs first, then module scoping.
// ============================================
// PART 10: PREPROCESSOR OPTIONS
// ============================================
// vite.config.js
export default {
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "./src/variables.scss";`,
},
},
},
}
// Variables are available in every .scss file.
The ten parts covered global CSS, ?inline, a module file, module import, combining modules with utilities, installing Sass, importing SCSS, a SCSS module, importing a SCSS module, and preprocessor options.
Quick Reference
CSS import types
| Import | Scope | Returns | Use case |
|---|---|---|---|
import './x.css' | Global | Nothing | Reset, base styles |
import './x.css?inline' | None (string) | CSS string | Shadow DOM, third-party |
import styles from './x.module.css' | Local | Class map | Component styles |
import './x.scss' | Global | Nothing | Global SCSS |
import styles from './x.module.scss' | Local | Class map | Component SCSS |
Preprocessor dependencies
| Extension | Package | Install |
|---|---|---|
.scss, .sass | sass | npm install -D sass |
.less | less | npm install -D less |
.styl | stylus | npm install -D stylus |
CSS Module naming
| Filename | Behavior |
|---|---|
Button.css | Global, no scoping |
Button.module.css | Scoped, returns class map |
Button.module.scss | Scoped SCSS, returns class map |
Configuration
| Option | Purpose |
|---|---|
css.preprocessorOptions | Pass options to the compiler |
css.modules | Configure module class-name generation |
css.postcss | Inline PostCSS config |
build.cssCodeSplit | Disable per-chunk CSS extraction |
Best Practices
✅ Do This:
// Import global CSS from the entry file
import './styles/global.css' // ✅
// Use .module.css for component-scoped styles
import styles from './Button.module.css' // ✅
# Install the preprocessor dependency
npm install -D sass // ✅
// Use ?inline for CSS that must not be injected
import css from './shadow.css?inline' // ✅
❌ Don’t Do This:
// Import a plain .css file for component styles
import './Button.css' // global, not scoped // ❌
// Expect CSS Modules without the .module extension
import styles from './Button.css' // no mapping // ❌
// Assume Sass works without the package installed
import './main.scss' // build error if sass missing // ❌
// Use ?inline when the styles should apply globally
import css from './global.css?inline' // not injected // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Styles leak to other components | Plain .css imported instead of .module.css | Rename to .module.css |
styles.className is undefined | Filename missing .module | Add the .module segment |
| Sass build error | sass not installed | npm install -D sass |
| Global variables unavailable in SCSS | No additionalData config | Add preprocessorOptions.scss.additionalData |
Styles not applied after ?inline | ?inline does not inject | Use a regular import for global styles |
| Production CSS order wrong | Import order changed | Keep CSS imports in one file; disable import sorting |
Real-World Examples
1. Global reset
import './styles/reset.css'
2. Component module
import styles from './Card.module.css'
<div className={styles.card} />
3. SCSS module
import styles from './Card.module.scss'
4. Inline CSS string
import css from './shadow.css?inline'
5. Install Sass
npm install -D sass
6. Global SCSS variables
additionalData: `@import "./src/variables.scss";`
7. Combine module and utility
className={`${styles.card} p-4`}
8. Dynamic CSS import
import('./heavy.css')
9. PostCSS auto-detection
// postcss.config.js is picked up automatically
10. Check module mapping
console.log(styles) // { button: 'Button_button__a1B2c' }
Visual
The three CSS paths
┌─────────────────────────────────────────────────────────────┐
│ PLAIN CSS │
│ import './global.css' │
│ → global scope │
│ → injected via <style> in dev, extracted in build │
│ │
│ CSS MODULE │
│ import styles from './Button.module.css' │
│ → local scope, hashed class names │
│ → returns { button: 'Button_button__hash' } │
│ │
│ PREPROCESSOR │
│ import './main.scss' │
│ → detected by extension │
│ → compiled by installed package │
│ → then treated as plain CSS or module │
└─────────────────────────────────────────────────────────────┘
CSS Module scoping
┌─────────────────────────────────────────────────────────────┐
│ Button.module.css │
│ .button { background: blue; } │
│ │
│ Compiles to: │
│ .Button_button__a1B2c { background: blue; } │
│ │
│ Card.module.css │
│ .button { background: red; } │
│ │
│ Compiles to: │
│ .Card_button__d4e5f { background: red; } │
│ │
│ No collision. Both classes exist, both apply correctly. │
└─────────────────────────────────────────────────────────────┘
Preprocessor detection
┌─────────────────────────────────────────────────────────────┐
│ FILE EXTENSION → PACKAGE │
│ │
│ .scss / .sass → sass │
│ .less → less │
│ .styl → stylus │
│ │
│ If the package is not installed: │
│ Build error naming the missing dependency │
│ │
│ If the package is installed: │
│ Compilation happens automatically, no config needed │
└─────────────────────────────────────────────────────────────┘
CSS code splitting
┌─────────────────────────────────────────────────────────────┐
│ ASYNC CHUNK CSS │
│ │
│ import('./HeavyComponent') │
│ │ │
│ ▼ │
│ HeavyComponent.js + HeavyComponent.css │
│ │ │
│ ▼ │
│ CSS file loaded via <link> before chunk evaluation │
│ │ │
│ ▼ │
│ No flash of unstyled content │
│ │
│ Disable with build.cssCodeSplit: false │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Global CSS import | import './file.css' |
| Scoped CSS import | import styles from './file.module.css' |
| Inline string import | import css from './file.css?inline' |
| Module activation | .module segment in filename |
| Module return value | Object mapping class names to hashes |
| Sass dependency | sass |
| Less dependency | less |
| Preprocessor options | css.preprocessorOptions |
| PostCSS config | Auto-detected from postcss.config.js |
| Async CSS loading | Automatic code splitting per chunk |
Key takeaways:
- CSS is a module in Vite’s graph. A plain import injects global styles and returns nothing. A
.module.cssimport returns a class-name map and scopes the styles to the importing component . - The filename is the configuration.
.module.cssactivates CSS Modules..scssactivates Sass..lessactivates Less. Vite detects the type and applies the transformer without anyvite.config.jschanges . - Preprocessors require only the dependency. Installing
sassorlessis the entire setup. Vite does not bundle them, and the build fails with a clear error if the file extension is present but the package is missing. ?inlineimports the processed CSS as a string without injecting it. This is the mechanism for shadow DOM stylesheets or for passing CSS to a library that expects text .- CSS code splitting is automatic. An async chunk’s CSS is extracted into a separate file and loaded before the chunk evaluates, which prevents a flash of unstyled content .
- PostCSS is auto-detected. A
postcss.config.jsin the project root is applied to all CSS without any Vite configuration .
Remember: Vite’s CSS handling is convention-driven. The file extension determines the transformer. The .module segment determines the scoping. The import statement determines whether the styles are injected or returned as a string. Global styles go in plain .css imports. Component styles go in .module.css files. Preprocessors go in .scss or .less files with the corresponding package installed. The result is a pipeline where the filename carries the intent, and the configuration file is only needed for options that the file type cannot express.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!