Skip to content

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):

bash
pnpm add -D @driftcss/vite@alpha

Put it before React:

ts
plugins: [driftcss({ css: ['src/**/*.css'] }), react()]

Options

OptionDefaultDescription
css[]Globs for stylesheets to extract. When set, only matching files are emptied and extracted.
traceImportstrueWhen 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
debugMaptrueEmit driftcss-map.json on vite build (not the dev server)
virtualModuleId'virtual:driftcss'Virtual CSS module id
injecttrueInject import 'virtual:driftcss' from index.html. Set false for library builds.
strictfalsetrue = fail on unknown static semantics; 'warn' = log
strictCssoffFail / warn on disallowed pass-through CSS (combinators, etc.)
statsfalseEmit driftcss-stats.json + console report on production build
purgefalseProduction build: keep only atoms used in JS (+ safelist)
compactClassNamesprod true, dev falseProd: c-0, c-1, … — dev: content hashes
preserveSelectors[]Semantic classes kept in CSS + JSX
noAtomizeAlias for preserveSelectors
safelist / safelistCssDynamic-only semantics / raw CSS prepend
safelistPatternsRegex list kept at purge time
classMapManual semantic → atom-string overlay (not extracted)
propertiesallAllowlist patterns (border-*, exact names)
ignorePropertiesSkip atomizing matching properties
extractCachetrueDisk cache under node_modules/.vite/driftcss-cache
dtson in devWrite src/driftcss.d.ts (or keep root driftcss.d.ts if it already exists)
hmrtrueInvalidate virtual CSS + JSX on CSS changes
sourceMapfalseCoarse file-level driftcss.css.map on build
dualOutputfalseDev 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

ts
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 for clsx(className, styles.btn)
  • className={styles.card} from virtual: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.