Letting Vite Handle Your CSS Pipeline
Modern CSS has closed a lot of the gaps that once made pre-processors like Sass feel indispensable. Native nesting and custom properties cover much of what we used to reach for a pre-processor for. But browser support still lags in places, and users aren't always on the latest versions. We can handle that with @supports feature detection, progressive enhancement, or polyfills — but build tools remain the most reliable way to smooth over those edges.
Vite, the build tool that took top honors in the State of JavaScript 2024 survey for both "Most Adopted Technology" and "Most Loved Library," handles CSS compilation with little to no configuration. While it's commonly associated with JavaScript frameworks like React, Vue, or Svelte, you can use it for a pure CSS workflow. You don't even need to write a line of JavaScript for basic compilation.
Setting Up a Bare-Bones Vite Project
Assuming you have Node and npm installed, create a new project from your terminal:
npm create vite@latest
The CLI will prompt you with a few questions. Keep it simple: choose Vanilla and JavaScript to get a starter template with a few example files.

Open the project folder in your IDE. If you want a clean slate, delete the assets/, public/, and src/ folders as well as .gitignore. You should be left with only index.html and package.json.

Replace the contents of index.html with a bare HTML template:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CSS Only Vite Project</title>
</head>
<body>
<!-- empty for now -->
</body>
</html>
Then install the project dependencies:
npm install

You'll now see a node_modules/ folder and a package-lock.json file. The former houses all installed packages, the latter ensures your team stays on consistent versions. You won't normally touch either directly, but both are required for Node and Vite to process your code.

Create a styles/ folder in the project root and add a main.css file inside it.
├── public/
├── styles/
| └── main.css
└──index.html
Link the stylesheet from the <head> of index.html:
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="https://css-tricks.com/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CSS Only Vite Project</title>
<!-- Main CSS -->
<link rel="stylesheet" href="styles/main.css">
</head>
Add a small bit of CSS to test with:
body {
background: green;
}
Run the build command:
npm run build
Vite scans your index.html for linked assets and compiles everything it finds, including CSS. The build is nearly instant, and you'll get a dist/ folder as the default output directory. Inside assets/, you'll find an index.css file (with a unique hash for cache busting) containing your minified CSS.


Re-running the build command every time you edit CSS would get tedious fast. Vite's development server solves that with hot module reloading:
npm run dev

The server runs on port 5173 by default. Navigate to http://localhost:5173/ to see your page (note the URL in the source has a typo; the correct port is 5173). Any changes to your HTML or CSS will reload instantly in the browser. Stop the server with CTRL+C or by closing the terminal.

Organizing Stylesheets with Cascade Layers
One nice pattern enabled by Vite is using separate CSS files named after cascade layers, then linking them all in index.html. This gives you fine-grained control over the cascade without needing a single monolithic stylesheet.
Start by defining your layer order in main.css:
/* styles/main.css */
@layer reset, layouts;
Now let's add a reset. The modern CSS reset from Mayank is available as an npm package:
npm install @acab/reset.css

Create a reset.css file that imports the reset as a cascade layer:
/* styles/reset.css */
@import '@acab/reset.css' layer(reset);
You can add more rules to the reset layer within this same file if needed.
/* styles/reset.css */
@import '@acab/reset.css' layer(reset);
@layer reset {
/* custom reset styles */
}
Note that the @import statement pulls from node_modules, which isn't part of the public build. This only works because Vite resolves it during compilation.
Now link both stylesheets in your HTML, keeping the order meaningful for the cascade:
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="https://css-tricks.com/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CSS Only Vite Project</title>
<link rel="stylesheet" href="styles/main.css">
<link rel="stylesheet" href="styles/reset.css">
</head>
Add one more stylesheet for layout-specific rules. Create styles/layouts.css and declare a layouts layer:
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="https://css-tricks.com/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CSS Only Vite Project</title>
<link rel="stylesheet" href="styles/main.css">
<link rel="stylesheet" href="styles/reset.css">
<link rel="stylesheet" href="styles/layouts.css">
</head>
/* styles/layouts.css */
@layer layouts {
/* layouts styles */
}
A handy snippet for this layer is the "Smol intrinsic container" from Stephanie Eckles' SmolCSS project:
/* styles/layouts.css */
@layer layouts {
.smol-container {
width: min(100% - 3rem, var(--container-max, 60ch));
margin-inline: auto;
}
}
This two-line container uses the min() function for responsive width and margin-inline: auto; for horizontal centering. The --container-max custom property lets you adjust the width dynamically.
Re-run npm run build and check dist/. Your compiled CSS will include the layer declarations from main.css, the full reset imported from reset.css, and the .smol-container class from layouts.css.
This gets you a long way without JavaScript. But if you want to push further, a tiny bit of JS unlocks broad browser compatibility for modern CSS features.
Extending the Build with Lightning CSS
Lightning CSS is a parser and post-processor that transforms modern CSS into backward-compatible styles. It can handle things like converting newer color functions to formats older browsers understand.
Install it as a development dependency:
npm install --save-dev lightningcss
To use it with Vite, you'll need a configuration file. Create vite.config.mjs:
// vite.config.mjs
export default {
css: {
transformer: 'lightningcss'
},
build: {
cssMinify: 'lightningcss'
}
};
Vite now uses Lightning CSS to transform and minify your styles. Test it with an oklch color in main.css:
/* main.css */
body {
background-color: oklch(51.98% 0.1768 142.5);
}
After rebuilding, check the output. Lightning CSS adds fallback properties for browsers that don't yet support newer color spaces:
/* dist/index.css */
body {
background-color: green;
background-color: color(display-p3 0.216141 0.494224 0.131781);
background-color: lab(46.2829% -47.5413 48.5542);
}
You can also specify browser targets using the browserslist package:
npm install -D browserslist
Update your Vite configuration to import both browserslist and Lightning CSS's helper module for targets:
// vite.config.mjs
import browserslist from 'browserslist';
import { browserslistToTargets } from 'lightningcss';
Then pass the browser targets into Vite's CSS configuration:
// vite.config.mjs
import browserslist from 'browserslist';
import { browserslistToTargets } from 'lightningcss';
export default {
css: {
transformer: 'lightningcss',
lightningcss: {
targets: browserslistToTargets(browserslist('>= 0.25%')),
}
},
build: {
cssMinify: 'lightningcss'
}
};
Lightning CSS offers many more options — enabling or disabling specific features, writing custom transforms, and more.
// vite.config.mjs
import browserslist from 'browserslist';
import { browserslistToTargets, Features } from 'lightningcss';
export default {
css: {
transformer: 'lightningcss',
lightningcss: {
targets: browserslistToTargets(browserslist('>= 0.25%')),
// Including `light-dark()` and `colors()` functions
include: Features.LightDark | Features.Colors,
}
},
build: {
cssMinify: 'lightningcss'
}
};
For a full rundown of available feature flags, check the Lightning CSS documentation.
When the extra tooling pays off
After walking through the setup, a fair question is whether this pipeline is worth the complexity. In many cases, the honest answer is no. For a simple site or a small component, hand-written CSS with a few custom properties will do the job without any build step.
Where this approach earns its keep is in larger, more structured projects. Building a design system, for instance, involves coordinating token values, component-level styles, and global resets across many files. Partialized source files that compile into a single, optimized stylesheet make that organization manageable. The same reasons apply to cross-browser compatibility: relying on Lightning CSS to handle prefixing and modern syntax translation removes a class of manual, error-prone work. And for anyone shipping CSS to production, the minification and optimization steps are essentially free wins once the tooling is in place.
The calculus shifts with project size and lifespan. For throwaway prototypes or brochure sites, adding Vite and Lightning CSS is overhead you don't need. For anything with sustained development and multiple contributors, the cost of the tooling is quickly repaid by cleaner source organization and more reliable output.



