Skip to content

Using Vite

Alpha (0.1) — this is the only install path. Stay on the npm alpha tag until 1.0. latest is not published.

DriftCSS works by reading your CSS, hashing each declaration into an atom, and rewriting static className / cn() / clsx() in JSX. Zero runtime style engine. You write ordinary semantic CSS.

Installation

01 — Create your project

Start a Vite + React + TypeScript app if you do not have one.

bash
npm create vite@latest my-project -- --template react-ts
cd my-project

02 — Install DriftCSS

bash
pnpm add -D @driftcss/vite@alpha

@driftcss/core (the native extractor) comes along as a dependency. Prebuilt binaries ship for common platforms. No Rust toolchain.

03 — Configure the Vite plugin

Put driftcss() before the React plugin so it sees the authored className strings.

ts
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import driftcss from '@driftcss/vite'

export default defineConfig({
  plugins: [
    driftcss({
      css: ['src/**/*.css'],
      strict: true,
    }),
    react(),
  ],
})

The plugin injects atom CSS into index.html. You do not import virtual:driftcss in your entry. Set inject: false only for library builds with no HTML.

strict: true fails the build on class names that do not appear in your CSS at all. Nested or grouped selectors (the default Vite starter) stay as pass-through CSS; those names are still known. A typo like className="btnn" still fails. Recommended in CI.

On first pnpm dev the plugin writes src/driftcss-env.d.ts so tsc -b sees the virtual modules (Vite 8 templates include only src). No tsconfig types edit.

04 — Start the dev server

bash
pnpm dev

Import your authored CSS so Vite sees it (and so HMR watches the file if you are not using css globs):

tsx
// src/main.tsx
import './button.css'

05 — Write a semantic class

css
/* src/button.css */
.btn {
  padding: 0.75rem 1.15rem;
  border-radius: 0.5rem;
  background: #e8a55c;
  color: #1a1102;
}
tsx
<button className="btn">Save</button>

In development the class names are content hashes (c-a1b2c3d4). Production uses compact ids (c-0, c-1, …) after shorthand expansion.

virtual:driftcss/classes is a default-export object: each semantic name maps to its atom string. Autocomplete works. A typo is a type error.

tsx
import styles from 'virtual:driftcss/classes'

export function Save() {
  return <button className={styles.btn}>Save</button>
}

Those values are already atoms at runtime, so the JSX transform leaves styles.btn alone. Fold or merge with cn() / clsx() the same way:

tsx
import clsx from 'clsx'
import styles from 'virtual:driftcss/classes'

<button className={clsx(styles.btn, on && styles.btnGhost)} />

In dev the plugin writes src/driftcss.d.ts (dts defaults on; it keeps a root driftcss.d.ts if you already have one). Vite 8's include: ["src"] picks the src/ file up. DriftClassName is a global union of your semantic names — you can type className?: DriftClassName without importing anything.

That is the whole golden path. Next, Vue, Nuxt, and the rest are not this alpha.


Optional next

Vite plugin optionscss globs, purge, dts, inject
CSS scopingCompound selectors break after names leave the DOM
Alpha scopeWhat extract does and does not atomize

npx driftcss init writes driftcss.config.mjs. The Vite plugin loads it (inline options win). CLI check / extract / watch / types do not — pass globs on the command line.

The settings UI in apps/app is a fuller Vite + React demo (clone this repo to run it).