| | |

Vite 08 ⚡ CSS Handling — Plain CSS, CSS Modules & preprocessors

⏱️ Estimated read: 10 min | |

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

ImportScopeReturnsUse case
import './x.css'GlobalNothingReset, base styles
import './x.css?inline'None (string)CSS stringShadow DOM, third-party
import styles from './x.module.css'LocalClass mapComponent styles
import './x.scss'GlobalNothingGlobal SCSS
import styles from './x.module.scss'LocalClass mapComponent SCSS

Preprocessor dependencies

ExtensionPackageInstall
.scss, .sasssassnpm install -D sass
.lesslessnpm install -D less
.stylstylusnpm install -D stylus

CSS Module naming

FilenameBehavior
Button.cssGlobal, no scoping
Button.module.cssScoped, returns class map
Button.module.scssScoped SCSS, returns class map

Configuration

OptionPurpose
css.preprocessorOptionsPass options to the compiler
css.modulesConfigure module class-name generation
css.postcssInline PostCSS config
build.cssCodeSplitDisable 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

PitfallWhy It HappensFix
Styles leak to other componentsPlain .css imported instead of .module.cssRename to .module.css
styles.className is undefinedFilename missing .moduleAdd the .module segment
Sass build errorsass not installednpm install -D sass
Global variables unavailable in SCSSNo additionalData configAdd preprocessorOptions.scss.additionalData
Styles not applied after ?inline?inline does not injectUse a regular import for global styles
Production CSS order wrongImport order changedKeep 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

ItemValue
Global CSS importimport './file.css'
Scoped CSS importimport styles from './file.module.css'
Inline string importimport css from './file.css?inline'
Module activation.module segment in filename
Module return valueObject mapping class names to hashes
Sass dependencysass
Less dependencyless
Preprocessor optionscss.preprocessorOptions
PostCSS configAuto-detected from postcss.config.js
Async CSS loadingAutomatic 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.css import returns a class-name map and scopes the styles to the importing component .
  • The filename is the configuration. .module.css activates CSS Modules. .scss activates Sass. .less activates Less. Vite detects the type and applies the transformer without any vite.config.js changes .
  • Preprocessors require only the dependency. Installing sass or less is 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.
  • ?inline imports 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.js in 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.

Was this helpful?


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!