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@alpha5 authored → 11 atoms · 8 longhands from shorthands0kb runtime
Live model — not the Rust compiler. Real builds hash with SHA-256 (c- + 8 hex), or compact c-0 names in production.
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.
Lean payload. Atoms dedupe by construction.
But your markup becomes the stylesheet. className sprawl, ordering rules to memorize, design intent scattered across JSX.
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.
Real selectors and your editor tooling. Static className / cn() / clsx() in JSX.
.btn { padding:0.75rem }
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 }
Rewritten classNames and one virtual stylesheet. No style engine in the browser — just CSS.
0kb runtime · virtual:driftcss
Every property: value pair becomes its own hashed atom — the smallest reusable unit of CSS.
Identical pairs collapse to one atom across your entire tree. A thousand padding:1rems ship once.
Stable, content-derived hashes. The same input always produces the same atoms — reproducible across machines and CI.
border becomes width, style and color longhands — so the color atom dedupes even when the shorthand differs.
Atoms carry their context: @media, @supports, @container, :hover, ::before.
eslint-plugin-driftcss flags JSX class strings that aren't in driftcss-map.json — unknown semantics, in the editor.
driftcss-map.json maps each atom back to its source selector and declaration. Written on vite build (on by default).
No style engine, no injection, no hydration cost. The browser just parses the CSS you shipped.
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.
$ pnpm add -D @driftcss/vite@alpha
// vite.config.tsimport { 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(),],})
/* 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>
@driftcss/coreRust · NAPIThe extraction engine. Parses, expands shorthands, hashes and dedupes atoms. Comes in as a dependency of the Vite plugin.
@driftcss/viteViteThe 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-driftcsslintFlags semantic class strings that aren't in driftcss-map.json, so unknown classes show up in the editor.
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.
className stringscn() / clsx()