Why Plumeria?
How Plumeria reclaims the predictability of CSS Modules without sacrificing the power of CSS-in-JS.
"CSS Modules are fine after all."
If you build web interfaces for a living, you have probably said this. After wrestling with runtime CSS-in-JS configuration, chasing specificity bugs across dynamic boundaries, or watching a utility-first framework bloat your markup, returning to the humble CSS Module feels like a relief.
That isn't a compromise made for lack of features. CSS Modules win because they are predictable: the CSS you write behaves exactly as written. There is no runtime parser guessing your intent, no injection-order races between chunks, and almost no runtime JavaScript — just a class mapping object.
But the safety has a price. You give up TypeScript-integrated styling, compile-time validation, dynamic theming, and seamless colocation.
Plumeria is designed to eliminate this compromise. It matches — and in several areas exceeds — the predictability of CSS Modules, while delivering the type-safe developer experience of a modern CSS-in-JS library.
The Zero-Trace Runtime
Try compiling this — note that the style is actually applied, not left unused:
import * as css from '@plumeria/core';
const styles = css.create({
box: {
padding: 16,
color: 'red'
}
});
export const Box = () => <div classStyle={styles.box}>Box</div>;Here is the entire JavaScript build output:
export const Box = () => <div className={'xqqbxt1d xq96bg3w'}>Box</div>;The declarations move to a generated stylesheet:
.xqqbxt1d { padding: 16px; }
.xq96bg3w { color: red; }The style still renders, yet import * as css from '@plumeria/core' and the entire css.create declaration have vanished. This is not dead-code elimination — nothing in this file is unused, and no bundler could remove a live call for you. The compiler resolves the class names statically and rewrites the call site, so the library never has a runtime form to eliminate in the first place. That disappearing import is the most concise illustration of a Zero-Trace Runtime — anything that shouldn't exist at runtime leaves no trace, not even an import statement. This isn't a best practice; it's a compiler contract.
Nothing of the library reaches the component, so a React Server Component keeps its styles with no 'use client' boundary to add.
Plumeria goes one step further than CSS Modules here. Where CSS Modules ship a class mapping object in your JS bundle, Plumeria inlines the final class name strings directly into the compiled output and deletes the css.create declarations. Not even a styling map remains in production JS. (See the Introduction for how the compilation works.)
Note the two class names in that output: one per property. Every property–value pair compiles to a global atomic class shared across your entire project, so your CSS stops growing in proportion to your component count — it plateaus.
Measured on identical Next.js apps, that is 1.83KB less client JavaScript than StyleX, which keeps its resolver — see the side-by-side benchmark.
Plumeria's strength doesn't come from flashy, single-purpose features. It comes from the cumulative effect of a few understated pillars that prove their value as your application grows.
Deterministic Cross-File Module Graph
Split, organize, and refactor styles across files without fear. Where traditional approaches let bundling order and import hierarchies alter CSS injection sequences — silently breaking layouts — Plumeria's output is determined strictly by the compiler's module graph. Your layouts look identical no matter how files are structured or in what order they are bundled.
Never Executes Your Application Code
Plumeria's compiler never executes your application code. It relies purely on fast static analysis (oxlint/ESLint) and direct AST rewrites, and the hand-written type definitions of @plumeria/core resolve without any evaluation. The build pipeline stays fast and lightweight — and it can never break on a runtime evaluation error, because there is no runtime evaluation.
Already on CSS Modules
The migration is a command, not a rewrite:
npx @plumeria/codemod migrate --from css-modulesEach *.module.css becomes a *.styles.ts beside it and its consumers are pointed at it — including the descendant rules, which have no combinator to translate into and become a marker and its extended styles instead. What cannot be converted is listed by file and line rather than guessed at. See @plumeria/codemod.
The command runs the other way as well:
npx @plumeria/codemod migrate --from plumeriaStyles go back to *.module.css, theme tokens and animations to global CSS, and the call sites to className. It is an export rather than a round trip — a dynamic style leaves as a custom property, a marker as the @container style() rule it compiles to — so what returns is the equivalent stylesheet, not the files you started from. Leaving is a command too.
Plumeria is for those who say, "I never compromise." Ready to try it? Start with Getting started.