Vite plugin
@driftcss/vite is a Vite plugin that runs before React: CSS is collected and extracted; TSX/JSX className / cn() / clsx() strings are rewritten at build time.
Install the alpha dist-tag until 1.0 (latest is not published):
pnpm add -D @driftcss/vite@alphaPut it before React:
plugins: [driftcss({ css: ['src/**/*.css'] }), react()]Options
| Option | Default | Description |
|---|---|---|
css | [] | Globs for stylesheets to extract. When set, only matching files are emptied and extracted. |
traceImports | true | When css is empty, intercept every .css file in the Vite module graph. Ignored once css globs are set. Does not parse CSS @import. |
classPrefix | 'c-' | Atom class prefix |
debugMap | true | Emit driftcss-map.json on vite build (not the dev server) |
virtualModuleId | 'virtual:driftcss' | Virtual CSS module id |
inject | true | Inject import 'virtual:driftcss' from index.html. Set false for library builds. |
strict | false | true = fail on unknown static semantics; 'warn' = log |
strictCss | off | Fail / warn on disallowed pass-through CSS (combinators, etc.) |
stats | false | Emit driftcss-stats.json + console report on production build |
purge | false | Production build: keep only atoms used in JS (+ safelist) |
compactClassNames | prod true, dev false | Prod: c-0, c-1, … — dev: content hashes |
preserveSelectors | [] | Semantic classes kept in CSS + JSX |
noAtomize | — | Alias for preserveSelectors |
safelist / safelistCss | — | Dynamic-only semantics / raw CSS prepend |
safelistPatterns | — | Regex list kept at purge time |
classMap | — | Manual semantic → atom-string overlay (not extracted) |
properties | all | Allowlist patterns (border-*, exact names) |
ignoreProperties | — | Skip atomizing matching properties |
extractCache | true | Disk cache under node_modules/.vite/driftcss-cache |
dts | on in dev | Write src/driftcss.d.ts (or keep root driftcss.d.ts if it already exists) |
hmr | true | Invalidate virtual CSS + JSX on CSS changes |
sourceMap | false | Coarse file-level driftcss.css.map on build |
dualOutput | false | Dev only: prepend authored semantic CSS before atoms |
Config file
The plugin loads the first existing file from the Vite project root:
driftcss.config.ts → .mts → .mjs → .js
File options first; inline driftcss({}) keys override (shallow merge). npx driftcss init writes driftcss.config.mjs. CLI check / extract / watch / types do not load this file.
Virtual modules
import 'virtual:driftcss' // atom CSS — injected into index.html by default
import styles from 'virtual:driftcss/classes' // { card: "c-… c-…", … }virtual:driftcss/classes is the typed map. styles.btn is already the atom string at runtime. The plugin writes src/driftcss-env.d.ts on first run so import 'virtual:driftcss' type-checks under Vite 8's include: ["src"]. With dts (on in dev) it also writes src/driftcss.d.ts — or keeps a root driftcss.d.ts if you already have one. DriftClassName is a global union of your semantic names.
Intercepted CSS files are replaced with a comment stub. Atoms load from the injected HTML import (or from an explicit import 'virtual:driftcss' when inject: false).
Static JSX supported
- String literals:
className="card" - Static templates:
className={`card border`} - Mixed templates:
className={`card ${x}`}— static tokens rewrite;${x}stays cn/clsx/classnames(including import aliases): whole-call fold when every arg is static; leftover rewrite forclsx(className, styles.btn)className={styles.card}fromvirtual:driftcss/classes— left as-is (already atoms at runtime)
Unknown extra strings (clsx(styles.btn, 'opacity-50')) do not warn unless strict is on.
Hoisted const x = clsx(...); className={x} is not visited — silent no-op.
Vue
Static class="…" in <template> is rewritten. :class, v-bind:class, and <style> blocks are not. Vue is not the launch surface.
Core API
Lower-level access via @driftcss/core: extract, extractFull, resolveClasses, purgeAtomRules.