CSSCSS in Build Tools (Vite, Webpack)

CSS in Build Tools (Vite, Webpack)

In any non-trivial modern frontend project, the CSS you write doesn't reach the browser unmodified. A build tool processes it first — bundling multiple stylesheets together, running it through PostCSS for autoprefixing and modern-syntax transforms, minifying it, and (for CSS Modules) rewriting class names to be collision-safe. Knowing roughly what your build tool does to your CSS matters most when something goes wrong: a missing prefix, an unexpectedly global class name, or a production-only bug all trace back to this pipeline.

What the CSS pipeline generally does
  • Bundling — collects every CSS file reachable from your JS/component imports into one (or a few) output stylesheets.

  • PostCSS transforms — runs plugins like Autoprefixer and postcss-preset-env so you can write modern or plain CSS and still support older target browsers.

  • Minification — strips whitespace/comments and shortens values for production output.

  • CSS Modules — for files named *.module.css, rewrites class names to unique, scoped identifiers so they can't accidentally collide with a class of the same name elsewhere in the app.

  • Source maps — in development, maps the generated CSS back to your original source files so DevTools shows the right file and line.

Vite: near-zero-config CSS
Vite treats CSS as a first-class import with almost no setup. A plain .css import just works, injecting the styles at runtime in development and extracting them into a real stylesheet for production:

JS
// main.jsx
import './styles.css'         // global CSS, just works
import styles from './Card.module.css'  // CSS Modules — scoped class names

function Card() {
  return <div className={styles.card}>Hello</div>
}

Sass/Less/Stylus support follows the same "just works" philosophy — Vite detects the file extension and only asks that you install the matching preprocessor package, with no loader configuration of your own:

Bash
npm install -D sass

JS
import './styles.scss' // works immediately once 'sass' is installed
Webpack: more explicit, more manual
Webpack doesn't understand CSS at all out of the box — every step has to be wired up explicitly through loaders and plugins in webpack.config.js:

JS
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader, // extracts CSS to its own file (prod)
          'css-loader',                // resolves @import / url() in CSS
          'postcss-loader',            // runs Autoprefixer etc.
        ],
      },
      {
        test: /\.module\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: { modules: true }, // enables CSS Modules scoping
          },
        ],
      },
    ],
  },
  plugins: [new MiniCssExtractPlugin()],
}

Vite

Webpack

Plain CSS import

Works with zero config

Requires style-loader/css-loader (dev) or MiniCssExtractPlugin (prod) configured

CSS Modules

Detected automatically via .module.css naming

Requires css-loader with modules: true explicitly set

Sass/Less

Install the preprocessor package, nothing else

Requires an additional loader (sass-loader, less-loader) wired into the rules array

Mental model

Convention-driven, hidden pipeline

Explicit pipeline you assemble yourself, rule by rule

Verbosity is Webpack's trade-off for control
Webpack's configuration is more explicit and more verbose than Vite's conventions, but that explicitness is also why large, highly customized legacy build setups are often still on Webpack — every step of the pipeline is a config object you can individually override. Vite intentionally hides most of this for the common case, using esbuild/Rollup under the hood, and only asks you to reach for manual plugin configuration when you step outside its conventions.
Why this matters for debugging

Most confusing "the CSS just isn't applying" bugs in a bundled project come down to a pipeline step doing something you didn't expect: a CSS Modules file whose class names got hashed differently than assumed, a PostCSS plugin silently rewriting a modern selector your target browsers don't support, or an import order that changed which rule wins in the final bundled output. Knowing the pipeline exists — bundle, transform, minify — is usually enough to know where to look first.

Inspect the actual built output when in doubt
When CSS behaves differently in production than in dev, open the built stylesheet directly (in the browser's Sources tab or the build output folder) rather than guessing. It shows you exactly what survived minification and bundling — often the fastest way to confirm whether a rule was transformed, reordered, or dropped entirely.