Alpha·Vite + React + TS·npm @alpha until 1.0

Semantic CSS,
shipped as atoms.

Write ordinary CSS classes and static className / cn() / clsx() in JSX. DriftCSS turns each declaration into hashed atoms that dedupe across components — no runtime style engine.

$pnpm add -D @driftcss/vite@alpha

5 authored → 11 atoms · 8 longhands from shorthands0kb runtime

button.cssauthored
previewsame declarations
virtual:driftcssatoms · className rewritten
.c-3ac738b0 { padding-top: 0.75rem }
.c-d202fca8 { padding-right: 1.15rem }
.c-09427a3e { padding-bottom: 0.75rem }
.c-4bef09d5 { padding-left: 1.15rem }
.c-49ce63b3 { border-top-left-radius: 0.5rem }
.c-c325e638 { border-top-right-radius: 0.5rem }
.c-b2b53894 { border-bottom-right-radius: 0.5rem }
.c-c487fb67 { border-bottom-left-radius: 0.5rem }
.c-7011681f { background-color: #e8a55c }
.c-7417aa41 { color: #1a1102 }
.c-50f32c80:hover { background-color: #f0b574 }
you write className="btn" → ships className="c-3ac738b0 c-d202fca8 c-09427a3e c-4bef09d5 c-49ce63b3 c-c325e638 c-b2b53894 c-c487fb67 c-7011681f c-7417aa41 c-50f32c80"

Live model — not the Rust compiler. Real builds hash with SHA-256 (c- + 8 hex), or compact c-0 names in production.

The tradeoff

You shouldn't have to choose between CSS you like and CSS that's lean.

semantic_css

Pleasant to write. Readable, composable, the CSS you already know.

But every component re-declares the same padding: 1rem. The stylesheet grows linearly with your component tree — you ship the bloat.

utility_css

Lean payload. Atoms dedupe by construction.

But your markup becomes the stylesheet. className sprawl, ordering rules to memorize, design intent scattered across JSX.

driftcss

Write the semantic CSS. Ship the atoms.

Extraction happens at build time. Your source stays readable; your bundle is deduped atomic CSS with zero runtime style engine.

How it works

Three steps. All of it at build time.

01

Author plain CSS

Real selectors and your editor tooling. Static className / cn() / clsx() in JSX.

.btn { padding:0.75rem }
02

DriftCSS extracts atoms

The Rust core expands shorthands, hashes every property/value pair into an atom, and dedupes across the whole tree.

.c-0 { padding-top:0.75rem }
03

Ship deduped atomic CSS

Rewritten classNames and one virtual stylesheet. No style engine in the browser — just CSS.

0kb runtime · virtual:driftcss
Capabilities

What the compiler does on the golden path.

Declaration-level extraction

Every property: value pair becomes its own hashed atom — the smallest reusable unit of CSS.

Cross-component dedupe

Identical pairs collapse to one atom across your entire tree. A thousand padding:1rems ship once.

Deterministic builds

Stable, content-derived hashes. The same input always produces the same atoms — reproducible across machines and CI.

Shorthand expansion

border becomes width, style and color longhands — so the color atom dedupes even when the shorthand differs.

Context-aware atoms

Atoms carry their context: @media, @supports, @container, :hover, ::before.

ESLint integration

eslint-plugin-driftcss flags JSX class strings that aren't in driftcss-map.json — unknown semantics, in the editor.

Debug maps

driftcss-map.json maps each atom back to its source selector and declaration. Written on vite build (on by default).

Zero runtime

No style engine, no injection, no hydration cost. The browser just parses the CSS you shipped.

Install & usage

Add the Vite plugin. Keep writing CSS.

Golden path: Vite + React + TypeScript. Stay on @driftcss/vite@alpha until 1.0. No new syntax — add the plugin and keep writing CSS. Atom CSS is injected as virtual:driftcss.

1 · install
$ pnpm add -D @driftcss/vite@alpha
2 · vite.config.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(),
  ],
})
3 · what you write → what ships
/* button.css — your source, untouched */
.btn {
  padding: 0.75rem 1.15rem;
  border-radius: 0.5rem;
  background: #e8a55c;
  color: #1a1102;
}
 
// Button.tsx — you write this
<button className="btn">Save</button>
 
// production — compact names; padding and radius expand to longhands
<button className="c-0 c-1 …">Save</button>
Packages

Four packages. Vite first.

@driftcss/coreRust · NAPI

The extraction engine. Parses, expands shorthands, hashes and dedupes atoms. Comes in as a dependency of the Vite plugin.

@driftcss/viteVite

The plugin you install. Collects CSS, rewrites static JSX className / cn() / clsx(), injects virtual:driftcss.

Unscoped so npx driftcss resolves. Commands: init, extract, check, watch, types.

eslint-plugin-driftcsslint

Flags semantic class strings that aren't in driftcss-map.json, so unknown classes show up in the editor.

ALPHA · honest status

Focused, not unfinished.

The golden path today is Vite + React + TypeScript, with static className, mixed templates (static segments rewrite), and static cn() / clsx(). Stay on the npm alpha tag until 1.0 — latest is not published. Next and Vue as launch surfaces are adapters — we'd rather extract a narrow surface correctly than a broad one approximately.

Static className strings
Static cn() / clsx()
Vite + React + TypeScript
Next, Vue golden path, combinator atoms — not yet