Compiling CSS With Vite and Lightning CSS

Home » Compiling CSS With Vite and Lightning CSS

Suppose you follow CSS feature development as closely as we do here at CSS-Tricks. In that case, you may be like me and eager to use many of these amazing tools but find browser support sometimes lagging behind what might be considered “modern” CSS (whatever that means).

Even if browser vendors all have a certain feature released, users might not have the latest versions!

We can certainly plan for this a number of ways:

  • feature detection with @supports
  • progressively enhanced designs
  • polyfills

For even extra help, we turn to build tools. Chances are, you’re already using some sort of build tool in your projects today. CSS developers are most likely familiar with CSS pre-processors (such as Sass or Less), but if you don’t know, these are tools capable of compiling many CSS files into one stylesheet. CSS pre-processors help make organizing CSS a lot easier, as you can move parts of CSS into related folders and import things as needed.

Pre-processors do not just provide organizational superpowers, though. Sass gave us a crazy list of features to work with, including:

  • extends
  • functions
  • loops
  • mixins
  • nesting
  • variables
  • …more, probably!

For a while, this big feature set provided a means of filling gaps missing from CSS, making Sass (or whatever preprocessor you fancy) feel like a necessity when starting a new project. CSS has evolved a lot since the release of Sass — we have so many of those features in CSS today — so it doesn’t quite feel that way anymore, especially now that we have native CSS nesting and custom properties.

Along with CSS pre-processors, there’s also the concept of post-processing. This type of tool usually helps transform compiled CSS in different ways, like auto-prefixing properties for different browser vendors, code minification, and more. PostCSS is the big one here, giving you tons of ways to manipulate and optimize your code, another step in the build pipeline.

In many implementations I’ve seen, the build pipeline typically runs roughly like this:

  1. Generate static assets
  2. Build application files
  3. Bundle for deployment

CSS is usually handled in that first part, which includes running CSS pre- and post-processors (though post-processing might also happen after Step 2). As mentioned, the continued evolution of CSS makes it less necessary for a tool such as Sass, so we might have an opportunity to save some time.

Vite for CSS

Awarded “Most Adopted Technology” and “Most Loved Library” from the State of JavaScript 2024 survey, Vite certainly seems to be one of the more popular build tools available. Vite is mainly used to build reactive JavaScript front-end frameworks, such as Angular, React, Svelte, and Vue (made by the same developer, of course). As the name implies, Vite is crazy fast and can be as simple or complex as you need it, and has become one of my favorite tools to work with.

Vite is mostly thought of as a JavaScript tool for JavaScript projects, but you can use it without writing any JavaScript at all. Vite works with Sass, though you still need to install Sass as a dependency to include it in the build pipeline. On the other hand, Vite also automatically supports compiling CSS with no extra steps. We can organize our CSS code how we see fit, with no or very minimal configuration necessary. Let’s check that out.

We will be using Node and npm to install Node packages, like Vite, as well as commands to run and build the project. If you do not have node or npm installed, please check out the download page on their website.

Navigate a terminal to a safe place to create a new project, then run:

npm create vite@latest

The command line interface will ask a few questions, you can keep it as simple as possible by choosing Vanilla and JavaScript which will provide you with a starter template including some no-frameworks-attached HTML, CSS, and JavaScript files to help get you started.

terminal displaying the output of running the command npm create vite@latest

Before running other commands, open the folder in your IDE (integrated development environment, such as VSCode) of choice so that we can inspect the project files and folders.

If you would like to follow along with me, delete the following files that are unnecessary for demonstration:

  • assets/
  • public/
  • src/
  • .gitignore

We should only have the following files left in out project folder:

  • index.html
  • package.json
VSCode file browser display two files: index.html and package.json

Let’s also replace the contents of index.html with an empty 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>

One last piece to set up is Vite’s dependencies, so let’s run the npm installation command:

npm install
terminal displaying the output of running the command npm install

A short sequence will occur in the terminal. Then we’ll see a new folder called node_modules/ and a package-lock.json file added in our file viewer.

  • node_modules is used to house all package files installed through node package manager, and allows us to import and use installed packages throughout our applications.
  • package-lock.json is a file usually used to make sure a development team is all using the same versions of packages and dependencies.
VSCode file browser displaying a node_modules folder, index.html file, package-lock.json file, and a package.json file

We most likely won’t need to touch these things, but they are necessary for Node and Vite to process our code during the build. Inside the project’s root folder, we can create a styles/ folder to contain the CSS we will write. Let’s create one file to begin with, main.css, which we can use to test out Vite.

├── public/
├── styles/
|   └── main.css
└──index.html

In our index.html file, inside the <head> section, we can include a <link> tag pointing to the CSS file:

<head>
  <meta charset="UTF-8" />
  <link rel="icon" type="image/svg+xml" href="/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>

Let’s add a bit of CSS to main.css:

body 
  background: green;

It’s not much, but it’s all we’ll need at the moment! In our terminal, we can now run the Vite build command using npm:

npm run build

With everything linked up properly, Vite will build things based on what is available within the index.html file, including our linked CSS files. The build will be very fast, and you’ll be returned to your terminal prompt.

Terminal displaying the output of the command npm run build, including the filesizes of compiled files
Vite will provide a brief report, showcasing the file sizes of the compiled project.

The newly generated dist/ folder is Vite’s default output directory, which we can open and see our processed files. Checking out assets/index.css (the filename will include a unique hash for cache busting), and you’ll see the code we wrote, minified here.

VSCode editor displaying a minified CSS file

Now that we know how to make Vite aware of our CSS, we will probably want to start writing more CSS for it to compile.

As quick as Vite is with our code, constantly re-running the build command would still get very tedious. Luckily, Vite provides its own development server, which includes a live environment with hot module reloading, making changes appear instantly in the browser. We can start the Vite development server by running the following terminal command:

npm run dev
Vite development server running in a terminal

Vite uses the default network port 5173 for the development server. Opening the http://localhost:5137/ address in your browser will display a blank screen with a green background.

Arc browser window, navigated to http://localhost:5173, a blank page with a green background

Adding any HTML to the index.html or CSS to main.css, Vite will reload the page to display changes. To stop the development server, use the keyboard shortcut CTRL+C or close the terminal to kill the process.

At this point, you pretty much know all you need to know about how to compile CSS files with Vite. Any CSS file you link up will be included in the built file.

Organizing CSS into Cascade Layers

One of the items on my 2025 CSS Wishlist is the ability to apply a cascade layer to a link tag. To me, this might be helpful to organize CSS in a meaningful ways, as well as fine control over the cascade, with the benefits cascade layers provide. Unfortunately, this is a rather difficult ask when considering the way browsers paint styles in the viewport. This type of functionality is being discussed between the CSS Working Group and TAG, but it’s unclear if it’ll move forward.

With Vite as our build tool, we can replicate the concept as a way to organize our built CSS. Inside the main.css file, let’s add the @layer at-rule to set the cascade order of our layers. I’ll use a couple of layers here for this demo, but feel free to customize this setup to your needs.

/* styles/main.css */
@layer reset, layouts;

This is all we’ll need inside our main.css, let’s create another file for our reset. I’m a fan of my friend Mayank‘s modern CSS reset, which is available as a Node package. We can install the reset by running the following terminal command:

npm install @acab/reset.css
Terminal displaying the output of running npm install @acab/reset.css

Now, we can import Mayank’s reset into our newly created reset.css file, as a cascade layer:

/* styles/reset.css */
@import '@acab/reset.css' layer(reset);

If there are any other reset layer stylings we want to include, we can open up another @layer reset block inside this file as well.

/* styles/reset.css */
@import '@acab/reset.css' layer(reset);

@layer reset 
  /* custom reset styles */

This @import statement is used to pull packages from the node_modules folder. This folder is not generally available in the built, public version of a website or application, so referencing this might cause problems if not handled properly.

Now that we have two files (main.css and reset.css), let’s link them up in our index.html file. Inside the <head> tag, let’s add them after <title>:

<head>
  <meta charset="UTF-8" />
  <link rel="icon" type="image/svg+xml" href="/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>

The idea here is we can add each CSS file, in the order we need them parsed. In this case, I’m planning to pull in each file named after the cascade layers setup in the main.css file. This may not work for every setup, but it is a helpful way to keep in mind the precedence of how cascade layers affect computed styles when rendered in a browser, as well as grouping similarly relevant files.

Since we’re in the index.html file, we’ll add a third CSS <link> for styles/layouts.css.

<head>
  <meta charset="UTF-8" />
  <link rel="icon" type="image/svg+xml" href="/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>

Create the styles/layouts.css file with the new @layer layouts declaration block, where we can add layout-specific stylings.

/* styles/layouts.css */
@layer layouts 
  /* layouts styles */

For some quick, easy, and awesome CSS snippets, I tend to refer to Stephanie EcklesSmolCSS project. Let’s grab the “Smol intrinsic container” code and include it within the layouts cascade layer:

/* styles/layouts.css */
@layer layouts 
  .smol-container 
    width: min(100% - 3rem, var(--container-max, 60ch));
    margin-inline: auto;
  

This powerful little, two-line container uses the CSS min() function to provide a responsive width, with margin-inline: auto; set to horizontally center itself and contain its child elements. We can also dynamically adjust the width using the --container-max custom property.

Now if we re-run the build command npm run build and check the dist/ folder, our compiled CSS file should contain:

  • Our cascade layer declarations from main.css
  • Mayank’s CSS reset fully imported from reset.css
  • The .smol-container class added from layouts.csss

As you can see, we can get quite far with Vite as our build tool without writing any JavaScript. However, if we choose to, we can extend our build’s capabilities even further by writing just a little bit of JavaScript.

Post-processing with LightningCSS

Lightning CSS is a CSS parser and post-processing tool that has a lot of nice features baked into it to help with cross-compatibility among browsers and browser versions. Lightning CSS can transform a lot of modern CSS into backward-compatible styles for you.

We can install Lightning CSS in our project with npm:

npm install --save-dev lightningcss

The --save-dev flag means the package will be installed as a development dependency, as it won’t be included with our built project. We can include it within our Vite build process, but first, we will need to write a tiny bit of JavaScript, a configuration file for Vite. Create a new file called: vite.config.mjs and inside add the following code:

// vite.config.mjs
export default 
  css: 
    transformer: 'lightningcss'
  ,
  build: 
    cssMinify: 'lightningcss'
  
;

Vite will now use LightningCSS to transform and minify CSS files. Now, let’s give it a test run using an oklch color. Inside main.css let’s add the following code:

/* main.css */
body 
  background-color: oklch(51.98% 0.1768 142.5);

Then re-running the Vite build command, we can see the background-color property added in the compiled CSS:

/* 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);

Lightning CSS converts the color white providing fallbacks available for browsers which might not support newer color types. Following the Lightning CSS documentation for using it with Vite, we can also specify browser versions to target by installing the browserslist package.

Browserslist will give us a way to specify browsers by matching certain conditions (try it out online!)

npm install -D browserslist

Inside our vite.config.mjs file, we can configure Lightning CSS further. Let’s import the browserslist package into the Vite configuration, as well as a module from the Lightning CSS package to help us use browserlist in our config:

// vite.config.mjs
import browserslist from 'browserslist';
import  browserslistToTargets  from 'lightningcss';

We can add configuration settings for lightningcss, containing the browser targets based on specified browser versions to 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'
  
;

There are lots of ways to extend Lightning CSS with Vite, such as enabling specific features, excluding features we won’t need, or writing our own custom transforms.

// vite.config.mjs
import browserslist from 'browserslist';
import  browserslistToTargets, Features  from 'lightningcss';

export default 
  css: 
    transformer: 'lightningcss',
    lightningcss:  Features.Colors,
    
  ,
  build: 
    cssMinify: 'lightningcss'
  
;

For a full list of the Lightning CSS features, check out their documentation on feature flags.

Is any of this necessary?

Reading through all this, you may be asking yourself if all of this is really necessary. The answer: absolutely not! But I think you can see the benefits of having access to partialized files that we can compile into unified stylesheets.

I doubt I’d go to these lengths for smaller projects, however, if building something with more complexity, such as a design system, I might reach for these tools for organizing code, cross-browser compatibility, and thoroughly optimizing compiled CSS.


Compiling CSS With Vite and Lightning CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

​ 

Leave a Comment

Your email address will not be published. Required fields are marked *