# AI Agent Resources
Source: https://plumeria.dev/docs/ai-agent-resources
Plumeria's documentation is available as plain Markdown for Claude, Codex, Gemini, ChatGPT, and other coding agents. Start with installation, then add only the references the task needs.
Always load the installation guide before asking an agent to write Plumeria. The compiler and bundler integration are part of the programming model—not optional setup around it.
## Start here [#start-here]
Copy this into your agent's prompt. It establishes the correct reading order and gives the agent the complete Plumeria rule set before it edits code.
```text title="Prompt for your coding agent"
Read these Plumeria documents in order before making changes:
1. Installation — https://plumeria.dev/docs/getting-started/installation.md
2. Rules for AI agents — https://plumeria.dev/docs/ai.md
3. Introduction and core usage — https://plumeria.dev/docs.md
Follow AI.md as authoritative. Inspect the existing project's bundler and framework,
then read the matching integration guide before changing configuration. Do not apply
runtime CSS-in-JS conventions to Plumeria: its styles are compiled statically.
```
The installation guide covers the package, styling-prop declaration, ESLint setup, and the compiler integration required for Next.js and Vite. For another bundler, append its guide from [Integrations](#integrations).
```text title="Minimum context"
https://plumeria.dev/docs/getting-started/installation.md
https://plumeria.dev/docs/ai.md
https://plumeria.dev/docs.md
```
## Add context for the task [#add-context-for-the-task]
Keep the three documents above, then append one of these focused sets.
### Writing components and styles [#writing-components-and-styles]
Use this set for everyday component work, selectors, composition, dynamic values, themes, animations, and style props.
```text title="Authoring context"
https://plumeria.dev/docs/getting-started/selector-rules.md
https://plumeria.dev/docs/reference/specificity.md
https://plumeria.dev/docs/api-reference/javascript/create.md
https://plumeria.dev/docs/api-reference/javascript/createStatic.md
https://plumeria.dev/docs/api-reference/javascript/createTheme.md
https://plumeria.dev/docs/api-reference/javascript/keyframes.md
https://plumeria.dev/docs/api-reference/javascript/marker.md
https://plumeria.dev/docs/api-reference/javascript/use.md
https://plumeria.dev/docs/api-reference/javascript/viewTransition.md
```
### Tooling, testing, and migration [#tooling-testing-and-migration]
Use this set when configuring linting, testing compiler output, or migrating an existing codebase.
```text title="Tooling context"
https://plumeria.dev/docs/getting-started/editor-integration.md
https://plumeria.dev/docs/testing.md
https://plumeria.dev/docs/api-reference/plugins/eslint-plugin.md
https://plumeria.dev/docs/api-reference/plugins/eslint-plugin/optional-rules.md
https://plumeria.dev/docs/api-reference/codemod.md
```
### Components and distribution [#components-and-distribution]
Use this set for component libraries, framework components, or development inspection.
```text title="Component context"
https://plumeria.dev/docs/integration/frameworks.md
https://plumeria.dev/docs/integration/component-library.md
https://plumeria.dev/docs/api-reference/components/headlessui.md
https://plumeria.dev/docs/api-reference/components/inspector.md
```
## Integrations [#integrations]
Load the guide that matches the project. Each page includes installation and a working configuration.
```text title="Bundler and framework integrations"
Next.js https://plumeria.dev/docs/integration/next.md
Vite https://plumeria.dev/docs/integration/vite.md
Bun https://plumeria.dev/docs/integration/bun.md
esbuild https://plumeria.dev/docs/integration/esbuild.md
Farm https://plumeria.dev/docs/integration/farm.md
Rollup https://plumeria.dev/docs/integration/rollup.md
Rolldown https://plumeria.dev/docs/integration/rolldown.md
Rspack https://plumeria.dev/docs/integration/rspack.md
webpack https://plumeria.dev/docs/integration/webpack.md
Frameworks https://plumeria.dev/docs/integration/frameworks.md
```
The lower-level plugin references document every configuration option:
```text title="Plugin references"
Next.js plugin https://plumeria.dev/docs/api-reference/plugins/next-plugin.md
Unplugin https://plumeria.dev/docs/api-reference/plugins/unplugin.md
ESLint plugin https://plumeria.dev/docs/api-reference/plugins/eslint-plugin.md
```
## Complete documentation map [#complete-documentation-map]
Use this map when an agent needs to discover material outside the focused sets above. It covers every documentation section, from concepts through API and CSS references.
| Area | What it contains | Markdown |
| :---------------- | :-------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------- |
| Getting started | Installation, selector rules, editor and ESLint setup | [Open section](/docs/getting-started) · [Markdown](/docs/getting-started.md) |
| Core concepts | Why Plumeria, compile-time behavior, composition | [Introduction](/docs) · [Why Plumeria?](/docs/why-plumeria) · [Category theory](/docs/category) |
| JavaScript API | `create`, static styles, themes, keyframes, markers, transitions, and `use` | [Open section](/docs/api-reference/javascript) · [Markdown](/docs/api-reference/javascript.md) |
| Components | Headless UI integration and the development inspector | [Open section](/docs/api-reference/components) · [Markdown](/docs/api-reference/components.md) |
| Plugins and tools | Next.js, unplugin, ESLint, and codemods | [Open section](/docs/api-reference/plugins) · [API index](/docs/api-reference) |
| Integrations | Nine bundlers, frameworks, and component-library distribution | [Open section](/docs/integration) · [Markdown](/docs/integration.md) |
| Testing | Compiler, component, jsdom, and type testing | [Open page](/docs/testing) · [Markdown](/docs/testing.md) |
| CSS reference | Specificity, colors, and supported pseudo-selectors | [Open section](/docs/reference) · [Markdown](/docs/reference.md) |
## Fetch Markdown [#fetch-markdown]
Add `.md` to any documentation URL to get the page as raw Markdown. The response includes its title and canonical URL.
```text title="Any page as Markdown"
https://plumeria.dev/docs/testing.md
https://plumeria.dev/docs/api-reference/javascript/create.md
https://plumeria.dev/docs/integration/webpack.md
```
For broad research, use the generated machine-readable indexes:
## Working examples [#working-examples]
The [`examples/` directory](https://github.com/zss-in-js/plumeria/tree/main/examples) contains compiling projects for Next.js, Vite, Rollup, Rspack, webpack, esbuild, Bun, Farm, and Rolldown, plus React Router, Vue, Svelte, Solid, Qwik, Preact, Astro, and Storybook. Point an agent to the matching example when it needs a complete configuration rather than an isolated snippet.
---
# AI.md
Source: https://plumeria.dev/docs/ai
This document is for AI assistants (Claude, Gemini, ChatGPT, and others) generating code with Plumeria. When writing Plumeria code, **always** follow the rules and patterns described here.
## Critical Rules (Quick Reference) [#critical-rules-quick-reference]
The complete rule set, distilled. Each rule is explained with examples in the sections below.
**MUST:**
* Call `css.create()` at module top level, never inside a component. (→ Forbidden Patterns)
* Bind styles with the `classStyle` prop, not `className`. (→ Mental Model)
* Import `@plumeria/core` in every file that uses Plumeria styles, including files that only consume imported styles. (→ Core Usage)
* Start nested selector keys with `:` (pseudo) or `[` (attribute). (→ Selector Rules)
* Explicitly declare a compound selector when simultaneous pseudo-class states set the same property. (→ Selector Rules)
* Compose with arrays; the right side always wins. (→ Core Usage)
**NEVER:**
* Pass a raw object to `classStyle`, e.g. `classStyle={{ color: 'red' }}`. (→ Forbidden Patterns)
* Call `css.create()` inside a component body. (→ Forbidden Patterns)
* Use the `&` self-reference character. (→ Selector Rules)
* Use `:has()`, `:is()`, or `:where()` — use `css.marker()` / `css.extended()` instead. (→ Selector Rules, Advanced APIs)
* Use child or descendant selectors (`.title`, `> div`) — style child elements directly. (→ Selector Rules)
* Nest a media/container query inside a pseudo-selector. The reverse (pseudo inside media) is allowed once. (→ Selector Rules)
* Mix `className` and `classStyle` on the same element. (→ Forbidden Patterns)
* Merge `css.use()` output with the inline `style` prop. (→ Dynamic Styling)
* Pass a received `Style` prop on to another component. Apply it on the element the component renders. (→ Styling Custom Components)
## Mental Model [#mental-model]
AI assistants often misapply runtime CSS-in-JS habits to Plumeria. Correct your mental model:
* **Styles are type schemas, not runtime objects.** Plumeria definitions are static schemas read by the compiler; they do not exist as JavaScript objects at runtime.
* **`css.create()` is a compiler directive, not an object factory.** Everything inside it is statically analyzed — the compiler must be able to read all styling information without executing code.
* **`classStyle` is NOT `className`.** Plumeria binds styles through its own `classStyle` prop. Never reach for `className` reflexively.
* **Zero-byte CSS in the JS bundle.** All CSS is resolved at build time. The abstraction layer (your definitions) and the output layer (generated CSS) are completely separated, so Plumeria has no side effects and high referential transparency.
## Core Usage [#core-usage]
Define styles at module top level with `css.create()`, bind them with `classStyle`:
```tsx
import * as css from '@plumeria/core';
const styles = css.create({
container: {
display: 'flex',
padding: '16px',
},
active: {
backgroundColor: 'blue',
},
});
export const MyComponent = ({ isActive }) => {
return (
Hello Plumeria
);
};
```
**Right-wins composition.** `classStyle` accepts arrays, ternaries, and conditional expressions. The right-most style always takes precedence: above, `styles.active` overrides `styles.container` when `isActive` is true. `css.use()` follows the same rule. Within a single style, do not rely on source order to resolve overlapping pseudo-class states; declare their compound selector explicitly (see Selector Rules).
**Cross-file imports.** Styles can be imported across files, but the consuming component file MUST contain `import "@plumeria/core";` — the import is how the compiler finds the file.
## Forbidden Patterns [#forbidden-patterns]
Never generate these patterns. They break static analysis or cause runtime errors.
❌ **Passing an object directly to `classStyle`:**
```tsx
Small Text
```
❌ **Creating styles inside a component body:**
```tsx
export const BadComponent = ({ dynamicColor }) => {
const dynamicStyles = css.create({
dynamic: { color: dynamicColor },
});
// ...
};
```
❌ **Mixing `className` and `classStyle` on the same element:**
```tsx
Text
```
❌ **Complex JS expressions in style values.** Only simple ternary operators are allowed, and only when necessary. Never put complex logic or function calls inside style values.
## Selector Rules [#selector-rules]
Plumeria supports nesting for pseudo-classes, pseudo-elements, and attribute selectors — under strict rules:
* **NO `&`.** Plumeria does not use the `&` self-reference character.
* **Nested keys MUST start with `:` or `[`.** Pseudo-classes/elements start with `:`; attribute selectors start with `[`.
* **NO `:has()`, `:is()`, `:where()`.** These are strictly forbidden. Use the paired `css.marker()` / `css.extended()` APIs (see Advanced APIs) for context-aware, parent-state, or descendant styling — they cover every use case of these pseudo-classes without breaking CSS atomicity.
* **NO child selectors.** Keys like `.title` or `> div` are not supported. Apply Plumeria styles directly to child elements instead.
* **Declare overlapping states explicitly.** Pseudo-classes such as `:hover`, `:focus`, and `:active` can match simultaneously. When two states set the same property, declare their compound selector with the value the intersection should take. If they set different properties, no compound selector is needed.
* **Media/container query nesting is one-directional.** A pseudo-selector may be nested inside a media/container query exactly once. The reverse — a media/container query inside a pseudo-selector — is forbidden and causes compiler/type errors.
```tsx
const styles = css.create({
button: {
backgroundColor: 'white',
padding: '16px',
// ✅ Pseudo-classes and elements (starts with :)
':hover': {
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
},
':active': {
boxShadow: '0 1px 4px rgba(0,0,0,0.2)',
},
// ✅ :hover and :active can match together; the intersection gets its own value
':hover:active': {
boxShadow: '0 2px 6px rgba(0,0,0,0.3)',
},
'::before': {
content: '""',
display: 'block',
},
// ✅ Attribute selectors (starts with [)
'[data-active="true"]': {
border: '2px solid blue',
},
// ✅ Pseudo-selector nested inside a media query (allowed once)
'@media (max-width: 768px)': {
padding: '8px',
':hover': {
boxShadow: 'none',
},
},
},
});
// ❌ INVALID: media query nested inside a pseudo-selector
const badStyles = css.create({
button: {
':hover': {
'@media (max-width: 768px)': {
backgroundColor: 'blue',
},
},
},
});
```
## Dynamic Styling [#dynamic-styling]
### Function keys [#function-keys]
For dynamic values (which would normally require React's inline `style` prop), define a **function key** in `css.create()`. Function keys compile to CSS variables — so unlike inline styles, they fully support `@media` and `@container` queries:
```tsx
const styles = css.create({
palette: (color: string) => ({
backgroundColor: color,
// Media/container queries work on dynamic styles
'@media (max-width: 600px)': {
backgroundColor: 'blue',
},
}),
});
// Usage:
```
### Bracket notation (variants) [#bracket-notation-variants]
The `css.variants()` API has been removed in favor of standard bracket notation (`styles[variant]`). The compiler turns these expressions into static lookup tables at build time:
```tsx
import * as css from '@plumeria/core';
// 1. Define variants in a dedicated, minimal css.create()
const sizeStyles = css.create({
small: { fontSize: '12px' },
medium: { fontSize: '16px' },
large: { fontSize: '20px' },
});
// 2. Type the variant (string literal union or keyof typeof)
type Size = keyof typeof sizeStyles;
interface ButtonProps {
size: Size;
children: React.ReactNode;
}
// 3. Resolve dynamically with bracket notation
export const Button = ({ size, children }: ButtonProps) => {
return ;
};
```
Best practices:
* ⚠️ **Keep variant `css.create()` calls minimal.** Every key in a `css.create()` used with bracket notation is compiled into the generated lookup table (e.g. `{"small":"...","medium":"..."}[size]`). Separate variants into dedicated calls; don't mix them with unrelated static styles.
* **Local variable assignment is supported.** `const currentStyle = sizeStyles[size]; ` works — the compiler traces local style variables and inlines them during JSX extraction.
### `css.use()` returns a static string [#cssuse-returns-a-static-string]
`css.use()` compiles to a static class name string. It has **no** integration with the inline `style` prop — never attempt to merge or combine `css.use()` output with `style`.
## Advanced APIs [#advanced-apis]
### `css.marker()` and `css.extended()` (paired descendant styling) [#cssmarker-and-cssextended-paired-descendant-styling]
These paired APIs enable context-aware styling (e.g. styling a child when the parent is hovered) without DOM combinators or `:has()`/`:is()`/`:where()`:
* **`css.marker(id, pseudo)`** — sets a CSS variable marker on the parent when the pseudo state is active. MUST be spread into the parent style: `...css.marker(...)`.
* **`css.extended(id, pseudo)`** — applies styles to descendants while the linked marker is active. Used as a computed key: `[css.extended(...)]`.
```tsx
import * as css from '@plumeria/core';
const styles = css.create({
parent: {
// 1. Set a marker with the unique id 'card' for the ':hover' state
...css.marker('card', ':hover'),
padding: '24px',
border: '1px solid #ccc',
},
child: {
transition: 'color 0.3s ease',
// 2. React to the 'card' marker being active
[css.extended('card', ':hover')]: {
color: 'blue',
// Further pseudo-classes can be nested here
':hover': {
color: 'darkblue',
},
},
},
});
export const Card = () => {
return (
Hover parent to make me blue!
);
};
```
At build time this compiles to an atomic container style query (`@container style(--x1f9k2q1-card-hover: 1)`) — zero runtime overhead, zero dependency on DOM hierarchy.
### `css.createStatic()` [#csscreatestatic]
Defines static variables (such as media query strings) that are inlined at build time, typically used as computed keys in `css.create()`:
```tsx
import * as css from '@plumeria/core';
export const breakpoints = css.createStatic({
xs: '@media (max-width: 480px)',
sm: '@media (max-width: 640px)',
md: '@media (max-width: 768px)',
lg: '@media (max-width: 1024px)',
});
export const styles = css.create({
container: {
[breakpoints.sm]: {
padding: 16,
},
[breakpoints.lg]: {
padding: 32,
},
},
});
```
### `css.createTheme()` [#csscreatetheme]
Defines scoped CSS variables for themes. First argument: the selector that activates the theme. Second argument: an object of `{ default, theme }` value pairs. Generated variable names are prefixed with a unique hash, so name collisions are impossible. Theme values are only compiled when actually used in a styling block.
```tsx
import * as css from '@plumeria/core';
// Class-based theme
export const theme = css.createTheme('.dark', {
text: {
default: '#333',
theme: '#eaeaea',
},
background: {
default: 'white',
theme: 'black',
},
});
// Attribute selectors and media queries are also supported:
// css.createTheme('[data-theme="dark"]', { ... });
// css.createTheme('@media (prefers-color-scheme: dark)', { ... });
```
### `css.keyframes()` [#csskeyframes]
Generates a unique `@keyframes` animation name (a hash inlined at build time) for use inside style rules:
```tsx
import * as css from '@plumeria/core';
const fadeIn = css.keyframes({
from: { opacity: 0 },
to: { opacity: 1 },
});
const styles = css.create({
card: {
transition: 'transform 0.3s ease',
':hover': {
animationName: fadeIn,
animationDuration: '0.5s',
},
},
});
```
### `css.viewTransition()` [#cssviewtransition]
Generates a unique `view-transition-name`, accepting animation definitions (`group`, `imagePair`, `new`, `old`) to customize View Transitions:
```tsx
import * as css from '@plumeria/core';
const fadeIn = css.keyframes({
from: { opacity: 0 },
to: { opacity: 1 },
});
const fadeOut = css.keyframes({
from: { opacity: 1 },
to: { opacity: 0 },
});
const longCrossFade = css.viewTransition({
old: {
animationName: fadeOut,
animationDuration: '1.2s',
},
new: {
animationName: fadeIn,
animationDuration: '1.2s',
},
});
export const transition = css.create({
name: {
viewTransitionName: longCrossFade,
},
});
// React usage:
// ...
```
## Styling Custom Components [#styling-custom-components]
There are exactly **3 patterns** for applying Plumeria styles to custom components. In all of them, compilation happens at the `classStyle` / `css.use()` call sites; the component itself just passes the compiled `className` / `style` through to the DOM.
> **Core principle**: Custom props typed as `Style` are statically traced by the compiler, so styles pass seamlessly across component boundaries.
### Pattern 1: Direct `classStyle` inside the component [#pattern-1-direct-classstyle-inside-the-component]
The component imports its own styles and applies them internally. Simplest pattern — fully self-contained:
```tsx
import React from 'react';
import * as css from '@plumeria/core';
const styles = css.create({
button: {
padding: '10px',
backgroundColor: 'navy',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
},
});
export const Button = ({ children }: { children: React.ReactNode }) => {
return (
// The compiler transforms classStyle → className here
);
};
// Usage
;
```
### Pattern 2: Passing a `Style` prop [#pattern-2-passing-a-style-prop]
`classStyle` itself is compiled away at the call site, so it cannot be used as a prop name on a custom component. Instead, define a custom prop (e.g. `styleArray`) typed as `css.Style`. The compiler statically traces it across component boundaries and resolves it into the internal element's `classStyle`. Composing as `[styles.text, styleArray]` lets call-site styles override the component's base styles (right-wins — see Core Usage): below, the call site's `fontSize: '24px'` overrides the internal `fontSize: '12px'`.
```tsx
// --- Button.tsx ---
import React from 'react';
import * as css from '@plumeria/core';
type ButtonProps = {
children: React.ReactNode;
styleArray?: css.Style;
};
// base style
const styles = css.create({
text: {
fontSize: '12px',
},
});
// Pass styleArray directly to the inner element's classStyle
export const Button = ({ children, styleArray }: ButtonProps) => {
return ;
};
// --- Usage (call site) ---
import * as css from '@plumeria/core';
import { Button } from './Button';
const styles = css.create({
primary: {
padding: '10px',
backgroundColor: 'navy',
color: '#fff',
},
text: {
fontSize: '24px',
},
});
// The compiler traces styleArray and resolves it statically
;
```
### Pattern 3: `className` bypass with `css.use()` [#pattern-3-classname-bypass-with-cssuse]
The component resolves the `Style` prop into a class name string with `css.use()` and passes it to `className`. The compiler traces the prop into the `css.use()` call the same way:
```tsx
// --- Button.tsx ---
import React from 'react';
import * as css from '@plumeria/core';
type ButtonProps = {
children: React.ReactNode;
styleArray?: css.Style;
};
// base style
const styles = css.create({
text: {
fontSize: '12px',
},
});
// Pass styleArray into css.use() to generate the className
export const Button = ({ children, styleArray }: ButtonProps) => {
return (
);
};
// --- Usage (call site) ---
import * as css from '@plumeria/core';
import { Button } from './Button';
const styles = css.create({
primary: {
padding: '10px',
backgroundColor: 'navy',
color: '#fff',
},
text: {
fontSize: '24px',
},
});
// The compiler traces styleArray inside css.use() and resolves it statically
;
```
### Anti-pattern: relaying a `Style` prop [#anti-pattern-relaying-a-style-prop]
A component that receives a `Style` prop MUST apply it — to `classStyle` or through `css.use()` — on an element it renders. A style prop that is never applied is a build error, which is what happens when it is handed to another component instead.
What decides this is **where the style ends up**, not how many style props a component takes. Passing styles in is always fine:
```tsx
// ✅ VALID: every value is a style, so any number of style props is fine
```
Forwarding one onward instead of applying it is not:
```tsx
// ❌ INVALID: headerStyle reaches no element of this component
export const Layout = ({ headerStyle }: { headerStyle?: css.Style }) => {
return ;
};
```
```
Plumeria: "headerStyle" is a style received through a prop but is never applied
to classStyle or css.use() here. Apply it on an element this component renders;
a style prop cannot be passed on to another component.
```
Write one of these instead:
```tsx
// ✅ Apply it on an element this component renders
export const Layout = ({ headerStyle }: { headerStyle?: css.Style }) => {
return ...;
};
// ✅ Or drop the middle component and style the one that owns the element
;
```
Merging under a base style — `[styles.base, headerStyle]` — is the supported shape. What is rejected is the *relay*: a prop that is only forwarded, never applied.
So the valid call site above stays valid only while `Layout` applies all three props itself. If it routes them onward to a grandchild, each unapplied prop is an error — which is the signal that the styles belong on the components that own those elements.
Prop drilling is an anti-pattern in React on its own terms; here it is also unresolvable, because the compiler would have to trace a style across an arbitrary chain of wrappers to build the lookup table. Rejecting it keeps the boundary decidable and the generated CSS exact.
### Summary [#summary]
| Pattern | Compilation site | Component's role |
| ---------------------- | -------------------------------- | --------------------------------- |
| 1. Direct `classStyle` | Inside the component | Self-contained styles |
| 2. `Style` prop | Traced and compiled | Receives and applies `Style` |
| 3. `className` bypass | Inside the component (`css.use`) | Resolves `Style` into `className` |
In every pattern the component **applies** the style it receives. None of them forward it.
## Testing [#testing]
**NEVER stub `@plumeria/core`.** It publishes types and no runtime. A `moduleNameMapper` entry or a hand-written mock makes the suite run, but every class name it produces is invented, so the test says nothing about the styles. If a runner cannot resolve the package, that is the signal to test a different layer, not to fake the package.
**NEVER assert a generated class name in a component test.** `xvdv6o3r` is one property-value pair, hashed. Adding a property to the style breaks every test that spelled the old list out. Assert what the component renders, holds, or branches on.
**To test what a style compiles to, call the transform directly.** It takes a source string and returns the rewritten code plus the stylesheet — no component, no DOM, no bundler — so Jest, Vitest and `node:test` all run it unchanged.
For a project on `@plumeria/unplugin`:
```js
const { unpluginFactory } = require('@plumeria/unplugin/factory');
const plugin = unpluginFactory(undefined, { framework: 'vite' });
const result = await plugin.transform.call({ addWatchFile: () => {} }, source, id);
const code = typeof result === 'string' ? result : (result?.code ?? '');
const cssId = code.match(/import "([^"]*\.zero\.css)"/)?.[1];
const resolved = await plugin.resolveId?.call({}, cssId);
const loaded = await plugin.load?.call({}, resolved?.id ?? resolved);
const css = typeof loaded === 'string' ? loaded : (loaded?.code ?? '');
```
For a project on `@plumeria/next-plugin`, call the loader its build runs, with a loader context of your own:
```js
const loader = require('@plumeria/turbopack-loader');
const fn = loader.default ?? loader;
const compile = (source) =>
new Promise((resolve, reject) => {
fn.call(
{
resourcePath: `${__dirname}/fixture.tsx`,
async: () => (err, content) => (err ? reject(err) : resolve(content)),
addDependency: () => {},
clearDependencies: () => {},
},
source,
);
});
```
The loader returns the rewritten code and writes its stylesheet only under `NODE_ENV=development`. Read class names from the returned code and derive them; do not hard-code them.
**To test component behaviour, use Vitest with the plugin in the config.** This is the only layer that needs the transform in the module pipeline.
```ts
export default defineConfig({
plugins: [react(), plumeria.vite()],
test: { environment: 'jsdom' },
});
```
**jsdom applies no stylesheet.** `document.styleSheets` is empty and `getComputedStyle` returns initial values. A test there can see which classes were attached, never what they do. Anything about the cascade — specificity, `@media`, `marker` with `extended` — belongs in an end-to-end test against a real browser.
## Toolchain Notes [#toolchain-notes]
**Compiler expectations.** The SWC compiler statically extracts `css.create()` calls, which is why they MUST sit at module top level. Prefer direct, clearly defined references — indirect variable references may be unanalyzable.
**ESLint guarantees.** `@plumeria/eslint-plugin` strictly enforces Plumeria's rules; code that satisfies it is safe to ship. It guarantees:
* **CSS property value validation** — invalid values are caught at lint time.
* **Property sort order** — a specific ordering is enforced.
* **Unused style detection** — unused styles are flagged automatically.
Follow the plugin's rules as documented in its `README.md`.
---
# Category theory
Source: https://plumeria.dev/docs/category
A category is a mathematical structure consisting of objects and morphisms.
If there are objects and morphisms, and the following two conditions are met, it is a category:
* Identity morphism
* Associativity
## First Condition (Identity morphism) [#first-condition-identity-morphism]
`create` is a monoid because it has a single object that satisfies the Identity morphism.
* `create -> create`
* `keyname -> keyname`
* `css-property -> css-property`
$A \circ A = A$
$A \oplus I = A$
Where the first expresses idempotency and the second expresses identity.
## Second Condition (Associativity) [#second-condition-associativity]
The law of associativity states that the result is the same regardless of the order in which the expressions are calculated.
Given `[A, B, C]`:
* `(A + B) + C`
* `A + (B + C)`
result in the same result, thus satisfying the compositional condition of the associative law.
$(A \oplus B) \oplus C = A \oplus (B \oplus C)$
## Composition of Monoids [#composition-of-monoids]
When this is assigned to styling,
{`[styles.A, styles.B, styles.C]`}
Then, the monoid `create` is a category with objects in composition.\
This is proof that a styling system closed to static analysis satisfies the axioms of category theory.
Ultimately, it always converges to a single result.\
$parse(A \oplus B \oplus C) \to \text{"classA classB classC"}$
---
# Introduction
Source: https://plumeria.dev/docs
Plumeria is a compile-time CSS-in-JS library for React. You write style objects in TypeScript, and the compiler turns them into static atomic CSS — while removing every trace of the library from your JavaScript bundle. No runtime code ships, no styles are computed in the browser.
```math
\mathtt{Plumeria(Type)} \xrightarrow{\text{Compilation}} \mathtt{Static\ Atomic\ CSS}
```
## Usage [#usage]
Styling is straightforward: define styles once with `css.create()`, and the compiler handles everything else. The atomic class names are determined at that point — all that remains is to attach them via the `classStyle` prop.
```tsx
import * as css from "@plumeria/core";
const styles = css.create({
text: {
fontSize: 16,
},
});
export const Footer = () => {
return
MyText
;
};
```
Styles compile to zero across file boundaries, too. You can colocate styles with components or share them across files — structure them to fit your project. The only requirement is that every file using Plumeria imports `@plumeria/core`, which is how the compiler finds it.
```tsx title="styles.ts"
import * as css from "@plumeria/core";
export const styles = css.create({
common: {
color: "gray",
},
});
```
```tsx title="Component.tsx"
import "@plumeria/core";
import { styles } from "./styles";
export const Component = () => {
return
Shared style
;
};
```
### Specificity [#specificity]
The `classStyle` prop accepts arrays, ternaries, and conditional expressions. Merging follows one rule: **the right side wins**. If `isActive` is true, the properties of `active` override the properties of `base` — always.
```tsx title="Component.tsx"
const styles = css.create({
base: {
color: "gray",
fontSize: 16,
},
active: {
color: "blue",
},
});
classStyle={[styles.base, isActive && styles.active]}
```
Because the compiler controls the merge, the cascade specificity problem disappears entirely. Writing the merge as an operation, where $\oplus$ combines styles left to right, disjoint styles simply concatenate:
$parse(A \oplus B \oplus C) \to \text{"classA classB classC"}$
And when two styles set the same property, the losing atom on the left is filtered out at compile time — it never reaches the output:
$parse(A_{color} \oplus B_{color}) \to \text{"classB"}$
A shorthand against a longhand is the exception: the longhand wins wherever the two meet, in either order. See [Specificity](/docs/reference/specificity).
## Types all the way down [#types-all-the-way-down]
Styling shouldn't require a runtime. Plumeria treats styles as **type schemas** rather than runtime objects — in fact, the package's only entry point is a `.d.ts` file. There is no JavaScript to import:
```json title="package.json"
{
"name": "@plumeria/core",
"exports": {
".": {
"types": "./lib/css.d.ts",
},
},
}
```
Static styles resolve to class names entirely at compile time. The only dynamic pieces — function keys and variant props — compile down to CSS variables, so even they leave no JavaScript behind. Every API compiles to nothing but static class names and CSS:
* No runtime bundle
* No runtime evaluation
* No runtime parsing
* No runtime dependencies
Of course, "zero-runtime" doesn't claim the cost vanishes — it claims the cost moves. All of the work happens at build time, and the browser only ever receives class names and CSS.
***
To learn what motivated this design, read [Why Plumeria?](/docs/why-plumeria). For integration with bundlers and the full API, see the [API reference](/docs/api-reference) and [Installation](/docs/getting-started/installation).
This site is built with Plumeria. The documentation app lives in the same repository and depends on the workspace packages, so every release is exercised here — and every experiment is tried here — before it ships.
---
# Testing
Source: https://plumeria.dev/docs/testing
Three questions get asked of a style, and each has its own answer. What did it compile to. Does the component that uses it behave. Does the page it renders actually look right.
## What a style compiles to [#what-a-style-compiles-to]
The transform is a function. Give it a source string and it hands back the rewritten code and the stylesheet, with no component, no DOM and no bundler in between — the same thing you would otherwise read off a `console.log`, in a form you can assert on.
```js title="compile.test.js"
const { unpluginFactory } = require('@plumeria/unplugin/factory');
const compile = async (source) => {
const plugin = unpluginFactory(undefined, { framework: 'vite' });
const result = await plugin.transform.call(
{ addWatchFile: () => {} },
source,
`${__dirname}/fixture.tsx`,
);
const code = typeof result === 'string' ? result : (result?.code ?? '');
const cssId = code.match(/import "([^"]*\.zero\.css)"/)?.[1];
const resolved = await plugin.resolveId?.call({}, cssId);
const loaded = await plugin.load?.call({}, resolved?.id ?? resolved);
return { code, css: typeof loaded === 'string' ? loaded : (loaded?.code ?? '') };
};
test('two properties become two atoms', async () => {
const { code, css } = await compile(`
import * as css from '@plumeria/core';
const styles = css.create({ box: { padding: 16, color: 'red' } });
export const A = () => ;
`);
expect(code).toMatch(/className=\{"\S+ \S+"\}/);
expect(css).toContain('padding: 16px');
expect(css).toContain('color: red');
});
```
Nothing here is runner-specific. It is plain asynchronous JavaScript, so Jest, Vitest and `node:test` all run it as it stands.
`@plumeria/unplugin/factory` and the default export of `@plumeria/turbopack-loader` are supported entry points for exactly this. The factory's hooks and the four properties the loader reads off its context are part of the published surface, and will not be changed under you.
### Next.js [#nextjs]
A project on [`@plumeria/next-plugin`](/docs/api-reference/plugins/next-plugin) tests the loader that its own build runs. It takes a webpack loader context rather than plugin hooks, which is six lines to stand up:
```js title="compile.test.js"
const loader = require('@plumeria/turbopack-loader');
const fn = loader.default ?? loader;
const compile = (source) =>
new Promise((resolve, reject) => {
const ctx = {
resourcePath: `${__dirname}/fixture.tsx`,
async: () => (err, content) => (err ? reject(err) : resolve(content)),
addDependency: () => {},
clearDependencies: () => {},
};
fn.call(ctx, source);
});
```
Add `@plumeria/turbopack-loader` to your dev dependencies rather than reaching it through the plugin.
The loader returns the rewritten code, and writes its stylesheet only under `NODE_ENV=development`. A test run leaves that file alone and sees the class names but no CSS. Set the variable if the stylesheet is what you came for — writes are taken under a lock, so parallel workers do not lose rules — or read it from the factory's `load()` above, which keeps it in memory and touches nothing.
## Components [#components]
Vitest runs on Vite, so it takes the plugin directly:
```ts title="vitest.config.ts"
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import plumeria from '@plumeria/unplugin';
export default defineConfig({
plugins: [react(), plumeria.vite()],
test: {
environment: 'jsdom',
},
});
```
The component is compiled by the plugin, and the styling prop is gone by the time it reaches the DOM:
```html
Vite + React
```
This is the one layer that needs the transform in the module pipeline. A runner that resolves `@plumeria/core` itself finds a package of types with no runtime behind it, and stops there.
### Never assert a generated class name [#never-assert-a-generated-class-name]
```ts
expect(heading.className).toBe('xvdv6o3r xggb8uiu xvazecna'); // don't
```
An atomic class name is one property–value pair, hashed. Add a property to the style and every test that spelled the old list out fails, having found nothing wrong with the component. Assert what the component does — the text it renders, the state it holds, the branch it took — and leave the names to the compile test above, which reads them out of the output instead of hard-coding them.
## What jsdom does not give you [#what-jsdom-does-not-give-you]
The class names are real, but no stylesheet is loaded into the document — `document.styleSheets` is empty, and `getComputedStyle` returns initial values for every property the styles set. A test can see that the right classes were attached; it cannot see what they do.
Anything that turns on the cascade — specificity between two styles, a `@media` branch, a `marker` and its `extended` styles — needs a real browser. Run those end to end, against the build, where the emitted stylesheet is actually applied.
## Types [#types]
Because the package is types-only, the API surface can be tested with nothing but `tsc`. Write the calls you expect to hold, mark the ones you expect to fail, and type-check the file:
```ts title="styles.test-d.ts"
import * as css from '@plumeria/core';
const styles = css.create({
text: { fontSize: '12px', color: 'red' },
variant: (size: number) => ({ width: `${size}px` }),
});
export const atomic: string = styles.text.color;
export const fromVariant: string = styles.variant(8).width;
// @ts-expect-error atomic class names are branded per property
export const crossed: css.AtomicClassNameFor<'color', 'red'> = styles.text.fontSize;
// @ts-expect-error the key was never defined
export const missing: string = styles.nope.color;
```
```sh title="Terminal"
tsc --noEmit
```
An `@ts-expect-error` that stops being an error is itself reported, as `TS2578: Unused '@ts-expect-error' directive`, so the file fails when a guarantee quietly disappears.
The work is already divided by the time a test runs. TypeScript offers the property names and values as you write them, and the [lint rules](/docs/api-reference/plugins/eslint-plugin) check what you wrote — spelling, values, pseudos, selectors, order-dependent overlap. What neither of them states is the type that comes back out, and that is what this file pins: the branding that keeps one property's class name from being passed off as another's, and keys that do not exist.
---
# Why Plumeria?
Source: https://plumeria.dev/docs/why-plumeria
## "CSS Modules are fine after all." [#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 [#the-zero-trace-runtime]
Try compiling this — note that the style is actually applied, not left unused:
```tsx
import * as css from '@plumeria/core';
const styles = css.create({
box: {
padding: 16,
color: 'red'
}
});
export const Box = () =>
Box
;
```
Here is the *entire* JavaScript build output:
```tsx
export const Box = () =>
Box
;
```
The declarations move to a generated stylesheet:
```css
.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](/docs) 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](https://github.com/refirst11/stylex-plumeria-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 [#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 [#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 [#already-on-css-modules]
The migration is a command, not a rewrite:
```sh
npx @plumeria/codemod migrate --from css-modules
```
Each `*.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](/docs/api-reference/javascript/marker) instead. What cannot be converted is listed by file and line rather than guessed at. See [`@plumeria/codemod`](/docs/api-reference/codemod).
The command runs the other way as well:
```sh
npx @plumeria/codemod migrate --from plumeria
```
Styles 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](/docs/getting-started).
---
# @plumeria/codemod
Source: https://plumeria.dev/docs/api-reference/codemod
[`@plumeria/codemod`](https://www.npmjs.com/package/@plumeria/codemod) rewrites Plumeria APIs across a codebase, and moves a codebase onto Plumeria in the first place.
| Transform | What it does |
| ---------------------------- | ------------------------------------------------------------------------------------------------------ |
| `migrate --from css-modules` | turns a CSS Modules stylesheet into `css.create` and points its consumers at it |
| `migrate --from plumeria` | exports `css.create` styles into CSS Modules, writes global styles, and updates JSX to use `className` |
| `rename-prop` | changes the styling prop everywhere it appears |
## Installation [#installation]
None. Run it with `npx` and it is gone again afterwards:
```sh title="Terminal"
npx @plumeria/codemod rename-prop classStyle sx
```
Install it only if you want it pinned in the repo:
```sh title="Terminal"
npm i --save-dev @plumeria/codemod
```
```sh title="Terminal"
yarn add -D @plumeria/codemod
```
```sh title="Terminal"
pnpm i --save-dev @plumeria/codemod
```
The package depends on ESLint and its TypeScript parser, and on nothing from Plumeria. It runs the same on a codebase you have not upgraded yet.
## Migrating from CSS Modules [#migrating-from-css-modules]
```sh title="Terminal"
npx @plumeria/codemod migrate --from css-modules
```
Every `*.module.css` under the paths becomes a `*.styles.ts` beside it, and the files importing it are rewritten — the import, the `className` prop, the class names, and `composes`.
```css title="Card.module.css"
.base { font-size: 12px }
.card { composes: base; padding: 16px }
.card:hover { background: teal }
.card .card-title { color: red }
```
```ts title="Card.styles.ts"
import * as css from '@plumeria/core';
export const styles = css.create({
base: { fontSize: 12 },
card: {
...css.marker('card', ':defined'),
padding: 16,
':hover': { background: 'teal' },
},
cardTitle: {
[css.extended('card', ':defined')]: { color: 'red' },
},
});
```
```diff title="Card.tsx"
+ import '@plumeria/core';
- import styles from './Card.module.css';
+ import { styles } from './Card.styles';
-
-
+
+
```
A descendant rule has no combinator to translate into — `no-combinator` rejects one — so it becomes [`css.marker`](/docs/api-reference/javascript/marker) on the parent and `css.extended` on the child. Both class names sit in the same selector, so which is which is read from the stylesheet.
Class names become camel-case keys, and a bare pixel length loses its unit, because `padding: 16` and `padding: '16px'` compile to the same rule.
### What each selector becomes [#what-each-selector-becomes]
| CSS Modules | Plumeria |
| :--------------------- | :--------------------------------------------------------------------------- |
| `.a .b`, `.a > .b` | `marker` on `a`, `extended` on `b` |
| `.a .b .c` | one marker per level, each gated by the marker above it |
| `.a[data-open="true"]` | `':is([data-open="true"])'` |
| `:global(.dark) .a` | `':is(.dark *)'` — a global class is never hashed, so it survives as written |
| `.a.b` | the pair rides on `b` |
| `.a h2` | a key of its own, carried onto the markup |
Three of these are worth reading twice.
**A chain gates each marker by the one above it.** `.a .b .c` writes `b`'s marker inside `a`'s `extended`, so a `.c` under a `.b` that sits outside any `.a` stays untouched — the nesting is reproduced, not flattened.
**A compound rides on the class written last.** `.a.b` only applies where both are present, and Plumeria hashes its classes, so no selector can state the pair. The call site already writes `[styles.a, styles.b]`, which is equivalent wherever the two appear together. Where `b` is also used on its own, it now carries what the pair set.
**A tag needs markup to land on.** `.a h2` has no class to hash, so the rule takes a key of its own and the consumer rewrite puts it on every `h2` it can see under an `a` in that file. A tag arriving through `children` is not visible there and is reported instead.
A sibling is the one relation nothing here states, and it is reported rather than converted. A style query reaches descendants, never siblings, so no marker carries the signal sideways. An ordinal is the usual replacement, but `:not(:first-child)` counts children of the parent while `+` reads the one element before it — the two agree only where every sibling carries the class. A header row or a separator in the list is enough to part them, and the stylesheet cannot see either. The rule is left where it is, named by file and line, because the markup that settles it is yours.
### Previewing the migration [#previewing-the-migration]
Start with `-d` or `--dry-run` to inspect the complete migration without creating any `*.styles.ts` files or rewriting their consumers:
```sh title="Terminal"
npx @plumeria/codemod migrate --from css-modules --dry-run
```
```
src/Card.module.css -> src/Card.styles.ts
1 stylesheet(s) would be converted, 1 consumer(s) rewritten.
Run without --dry-run to apply.
```
Paths can follow the flag as usual. Rules that cannot be converted are reported in the same run, and the exit code remains 1 while any such rules are present. Remove `--dry-run` to generate the modules and rewrite their consumers.
### What it reports instead of converting [#what-it-reports-instead-of-converting]
```
src/Card.module.css
9:3 composes-external composes: base from './shared.module.css'
A class composed from another file is not resolved. Convert that
stylesheet too, then pass both at the call site as an array.
12:1 sibling-combinator .item + .item
A container query reaches descendants, never siblings, so no marker
states this. Whether an ordinal such as `:not(:first-child)` is
equivalent depends on the markup.
```
A `composes` reaching into another file, a sibling combinator, and a selector naming no local class — an id, an element on its own — are named rather than guessed at, and the original rule is left where it is. The exit code is 1 while anything remains, so the command composes with a script.
### The order the array is written in [#the-order-the-array-is-written-in]
A class list carries no order. These two render the same page:
```tsx
```
Where two classes of equal specificity set one property, the stylesheet decides and not the element. The styling prop is the other way round: the merge reads the array, and the entry written last wins.
So the array is written in stylesheet order rather than in the order the class list happened to carry:
```css title="Card.module.css"
.a { color: red }
.b { color: blue }
```
```diff title="Card.tsx"
-
+
```
`blue` wins on both sides. Reading the class list instead would have handed it to `red`.
Two stylesheets have no order between them that one file can be sure of — that is the bundler's — so reads of different modules keep the slots they were given, and only the reads of one module are sorted among themselves.
A class written twice with another one between the two is the case a single place cannot answer: the merged key would take the later position for every declaration it holds, including the ones the class between them outranks. Where the two share a property, that is reported as `split-order` rather than guessed at.
### Where the order cannot be carried across [#where-the-order-cannot-be-carried-across]
An at-rule carries no specificity in CSS, so a plain rule written after a conditional one wins wherever the condition holds. Plumeria reads it the other way: a declaration under an at-rule [outranks a plain one](/docs/reference/specificity), whichever at-rule it is and however deeply they nest. No array can put that back.
Only an element carrying both classes is affected by it, which is a fact about the call site rather than the stylesheet — a print rule sitting beside a screen rule settles nothing until something wears both. So the pairs are read where the class list is:
```css title="Card.module.css"
@media print {
.printOnly { color: black }
}
.screenOnly { color: purple }
```
Nothing is reported for that on its own, and both classes convert. An element carrying the two together is what cannot be answered, and there the stylesheet is held for its consumers.
A class that does it to itself needs no call site to show it — one key holds both declarations and has no array to settle them with — so `@media print { .a { color } }` followed by `.a { color }`, or `padding-top` followed by `padding` inside one rule, is reported as `rank-order` when the stylesheet is read. Both are the same shape, and writing the higher-ranked declaration last settles either.
The at-rule is one of two things Plumeria ranks by. The other is the shorthand graph: a longhand [outranks the shorthand that writes it](/docs/reference/specificity), so `.a { padding-top }` followed by `.b { padding }` reads the same way — CSS lets the later shorthand reset the longhand, and Plumeria does not. Both are the one rank, counted in steps: one for being written, one more under an at-rule, one per step down the graph. Two declarations Plumeria ranks alike are level, and there the array carries their order.
The pair is read one declaration at a time rather than by where the class ended up. A class that writes `color` first and `margin` last sits late in the stylesheet on account of the `margin`, which says nothing about the `color` that disagrees. A `composes` goes through the same reading, because the composed class lands on the element beside the one that named it.
### When a consumer reads what was refused [#when-a-consumer-reads-what-was-refused]
A refusal leaves the rule in the stylesheet, which is harmless until a component reads the class it was written for. Half of that class would arrive as a Plumeria key and half would stay behind in a file the component no longer imports — a rule that silently stops applying, or a key that does not exist at all.
Rather than write that, the migration holds the stylesheet back whole:
```
src/Card.module.css
12:1 sibling-combinator .row + .row
A container query reaches descendants, never siblings, so no marker
states this.
0:0 held-for-consumer
`row` is read by a consumer, so nothing was written and no consumer
was moved. Settle the rules above and run again.
```
No `*.styles.ts` is written and no call site is touched, so the component keeps reading the stylesheet it always read and the project still builds. Other stylesheets in the same run convert as usual — the hold is per stylesheet, not per run.
Nothing being written is what makes the second attempt work. A module left on disk would be reported as `target-exists` on the next run and never picked up again; because the hold writes nothing, settling the rule it named and running again converts the file.
A class the migration converted in full is never held. The hold reads the
rules that were refused, so a stylesheet converts as soon as no component
depends on one of them.
## Migrating from Plumeria [#migrating-from-plumeria]
```sh title="Terminal"
npx @plumeria/codemod migrate --from plumeria
```
Exports styles written with `css.create` to CSS Modules, moves theme tokens and animation definitions into global CSS, and updates component files to use `className`.
Run `eslint --fix` with the recommended config before exporting, so shorthand and property-overlap rules have settled the styles first.
```tsx title="Card.tsx"
import * as css from '@plumeria/core';
export const styles = css.create({
base: { fontSize: 12 },
card: {
padding: 16,
':hover': { background: 'teal' },
},
size: (width: number) => ({ width }),
});
export function Card({ width }: { width: number }) {
return ;
}
```
```css title="Card.module.css"
.base {
font-size: 12px;
}
.card {
padding: 16px;
}
.card:hover {
background: teal;
}
.size {
width: var(--styles-size-width);
}
```
```diff title="Card.tsx"
- import * as css from '@plumeria/core';
+ import styles from './Card.module.css';
+ export { styles };
- export const styles = css.create({
- base: { fontSize: 12 },
- card: {
- padding: 16,
- ':hover': { background: 'teal' },
- },
- size: (width: number) => ({ width }),
- });
export function Card({ width }: { width: number }) {
- return ;
+ return (
+
+ );
}
```
### Global styles: themes and animations [#global-styles-themes-and-animations]
Definitions created with `css.createTheme`, `css.keyframes`, and `css.viewTransition` are extracted and appended to `src/styles/global.css` (or `styles/global.css`):
* **`css.createTheme`**: Emits root CSS variables (`:where(:root)`) and selector/at-rule scoped variables (e.g. `.dark`, `@media (prefers-color-scheme: dark)`). References across modules become `var(--hash-tokenName)`.
* **`css.keyframes`**: Emits `@keyframes kf-xxxx` in global CSS. References in styles are replaced by the generated keyframe identifier.
* **`css.viewTransition`**: Emits `::view-transition-*` pseudo-element rules in global CSS and assigns `view-transition-name: vt-xxxx`.
* **`css.createStatic`**: Constant values are statically evaluated and inlined directly into generated CSS rules.
### Where definitions may live [#where-definitions-may-live]
Definitions do not have to sit in the file that uses them. Import specifiers are resolved through `tsconfig.json` `paths`, directory `index` files, and filenames that carry a dot, so a `css.createTheme`, `css.keyframes`, or `css.createStatic` reached under an alias is inlined rather than reported as dynamic.
An import that only existed to feed the styles is removed. A binding read outside the exported styles — `window.matchMedia(breakpoints.lg)`, for instance — is replaced by the value it resolved to, because a theme token, a static, and a keyframe name are all plain strings once the migration is done.
A file that only defines styles and never uses them is left exactly as it is, the way `--from css-modules` leaves the original stylesheet in place. Its consumers are pointed at the generated `*.module.css` instead:
```diff title="Card.tsx"
- import { styles } from './Card.styles';
+ import styles from './Card.module.css';
export const Card = () => (
-
+
);
```
### Markers and extended styles [#markers-and-extended-styles]
`css.marker` and `css.extended` are expanded into the custom property and the `@container style()` rule they compile to, using the same variable name the compiler generates:
```css title="MarkerExtended.module.css"
.grand:hover {
--xl0a2l54-grand-hover: 1;
}
@container style(--xl0a2l54-grand-hover: 1) {
.child {
color: #0066cc;
}
}
```
### Several `css.create` calls in one file [#several-csscreate-calls-in-one-file]
Every `css.create` in a file is written to the same stylesheet, and the later bindings are read from the shared module. A key an earlier call already claimed is renamed to `-`:
```diff title="Button.tsx"
- const styles = css.create({ base: { color: 'red' } });
- const hoverStyles = css.create({ base: { color: 'blue' } });
+ import styles from './Button.module.css';
export const Button = () => (
-
+
);
```
### A style read by a named key [#a-style-read-by-a-named-key]
A variant is often picked with a bracket, not a dot. The key is resolved to the class it names, in the same file or through an import, and a constant left naming nothing is removed with it:
```diff title="Variant.tsx"
- const size = 'small';
- const color = 'primary';
-
+
```
Only a `const` initialised with a string literal is followed, and a name declared twice in one file is left alone rather than guessed at. A key the call site computes at runtime has no class to name, so it is reported as `dynamic-style-access` and its definitions stay in Plumeria.
### Dynamic function styles [#dynamic-function-styles]
Function styles such as `(width) => ({ width })` are converted into CSS custom properties (e.g. `----`). In JSX, the codemod passes argument values through the inline `style` prop:
```diff
-
+
```
The key is written as a computed string because React's `CSSProperties` has no room for a custom property.
If an existing `style` prop is present, the CSS variables are merged into it.
### Style composition [#style-composition]
An array with no condition in it collapses into a single class that `composes` its members, so the call site reads as one name:
```diff
-
+
```
```css title="ButtonLink.module.css"
.buttonBaseMedium {
composes: button base medium;
}
```
`composes` copies nothing, so the stylesheet does not grow, and the reverse migration reads it straight back into an array.
Any other array is joined as it stands. `filter` is added only where a condition
can make a member falsy, so that branch drops out at runtime:
```diff
-
+
-
+
```
### Previewing the migration [#previewing-the-migration-1]
Start with `-d` or `--dry-run` to inspect the exported style modules and rewritten sources without writing files:
```sh title="Terminal"
npx @plumeria/codemod migrate --from plumeria --dry-run
```
```
src/Card.tsx -> src/Card.module.css
global styles -> src/styles/global.css
1 style module(s) would be exported, 1 source file(s) rewritten.
Run without --dry-run to apply.
```
### What it reports instead of converting [#what-it-reports-instead-of-converting-1]
Constructs that cannot be statically resolved or exported without changing runtime behavior are reported, and the original Plumeria definitions are left in place:
```
src/Card.tsx
8:5 dynamic-value
The value of `color` cannot be represented statically.
12:3 spread-create
Top-level spreads cannot name a CSS Module class.
```
* Dynamic values, computed properties, or runtime function calls in style objects
* Object spreads in top-level `css.create` or style definitions, other than `css.marker`
* Existing `*.module.css` target files are reported as `target-exists` and will not be overwritten
### Composition order after the export [#composition-order-after-the-export]
`classStyle={[styles.b, styles.a]}` resolves `color` to `styles.a`: the rightmost style wins the merge. A stylesheet has no array to read — both classes land on the element and the cascade decides — so the export reproduces the array order by three means, in order of cost.
**Declaration order.** Every composition in the project is read as a constraint, and the classes are emitted in an order that satisfies all of them at once. Most projects need nothing else, and nothing is duplicated.
A constraint is only recorded where the two classes actually disagree, measured per at-rule and per selector. `.wide { width }` and `.narrow { @media (…) { width } }` name the same property but never contend — an at-rule declaration already outranks a base one — so they impose no order.
Not every disagreement is the array's to settle. Where one property covers another — `padding` against `padding-top` — Plumeria gives the longhand more specificity, so it wins wherever the two meet and the call site order never enters into it. The export writes the shorthand first instead, inside a rule and between rules alike, which reaches the same answer without the array. Only a partial overlap, `borderColor` against `borderTop`, is left for the call site to decide.
**A composed class.** When two call sites compose the same pair in opposite orders, no declaration order satisfies both. If either call site has no condition in it, it collapses into a class of its own and stops asking.
**An override class.** What is left is a call site whose members are decided at runtime. It carries an extra class holding only the disputed declarations, applied under the same condition as the style that has to win:
```diff title="Panel.tsx"
-
+
```
```css title="Panel.module.css"
.surfaceOverRaised:not(#\#) {
padding: 8px;
color: black;
}
```
`:not(#\#)` matches no element and adds an id to the specificity, so the override outranks both classes wherever it lands. It is named after the call site that needs it, so two call sites never fight over one.
Anything none of the three settles is reported as `composition-order` and left in Plumeria.
### Running it twice [#running-it-twice]
The migration is idempotent. The first run converts; every run after it is a fixed point:
$f(f(x)) = f(x)$
A second run finds no `css.create` to export, reports `target-exists` for the definition files it deliberately left behind, and writes nothing. The generated block in `global.css` is fenced by a marker comment and replaced rather than appended, so it does not grow. This holds even when the first run left styles behind: clear a report, run again, and only the newly resolvable files move.
Plumeria's own documentation site and end-to-end site go through the whole cycle on every CI run — exported, exported again, adopted back, adopted back again, and exported once more. Each pass has something to prove: the export must not leave a type error behind or name a class it never wrote, the second export must move nothing, the adoption must not leave a consumer reading a `*.module.css`, the second adoption must move nothing, and the project that came back must export again without a new report or a new type error.
Byte equality with the first export is not the claim. A round trip folds a constant that only named a style key and gives a merged class a name, and exporting that reaches further than the first export did — the point is that nothing breaks and nothing new is refused. The exported site is also served and compared against the Plumeria one screenshot by screenshot, so the cycle is measured on the rendered page and not only in the source.
## Renaming the styling prop [#renaming-the-styling-prop]
```sh title="Terminal"
npx @plumeria/codemod rename-prop [paths...]
```
`paths` default to the current directory. `dist`, `build`, `out`, `.next` and `coverage` are always skipped, and the transform reads `.js`, `.jsx`, `.mjs`, `.cjs`, `.ts`, `.tsx`, `.mts` and `.cts`.
| Option | Description |
| ----------------- | ---------------------------------------- |
| `-d`, `--dry-run` | report what would change without writing |
| `--no-types` | leave TypeScript declarations untouched |
| `-h`, `--help` | show usage |
| `-v`, `--version` | show the version |
Start with `--dry-run`:
```sh title="Terminal"
npx @plumeria/codemod rename-prop classStyle sx --dry-run
```
```
src/Card.tsx 3
src/page.tsx 1
4 occurrence(s) in 2 file(s) would become "sx".
Run without --dry-run to apply.
2 occurrence(s) need a manual rename — "classStyle" is bound to a local name there:
src/Card.tsx:8:24
src/Card.tsx:12:71
```
Drop the flag to apply. The report is the same, with the count confirmed rather than predicted:
```
✔ renamed 4 occurrence(s) of "classStyle" to "sx" in 2 file(s).
```
If the git working tree is dirty, it says so before writing — the rewrite is much easier to undo from a clean tree.
## What it rewrites [#what-it-rewrites]
**JSX attributes.**
```diff
-
+
```
**Property signatures annotated with `Style`.** That covers your `plumeria.d.ts` as well as the props of your own components:
```diff
interface CardProps {
- classStyle?: Style;
+ sx?: Style;
}
```
The annotation has to be `Style` or `Plumeria.Style`, so an unrelated interface that happens to use the same key is left alone. Pass `--no-types` to skip declarations entirely.
## What it reports instead of rewriting [#what-it-reports-instead-of-rewriting]
Where the prop is destructured, or read off an object, the name is a local binding and not only a prop:
```tsx
const Card = ({ classStyle }: CardProps) => ;
const Panel = (props: CardProps) => ;
```
The attribute is safe to rewrite; the binding is a judgement call about the surrounding code. Those sites are listed with their positions so you can finish them by hand:
```diff
const Card = ({ classStyle, title }: CardProps) => (
-
{title}
+
{title}
);
```
Renaming the parameter as well is the usual next step — the codemod just will not guess it for you.
## Names it refuses [#names-it-refuses]
`` has to be a valid identifier, has to differ from ``, and cannot be a prop React already handles — `className`, `key`, `ref`, `children` or `style`:
```
✖ "style" is already used by React
```
Files that fail to parse are reported at the end and the command exits with `1`, so a broken file cannot be mistaken for a file with nothing to rename.
## After the rewrite [#after-the-rewrite]
The codemod changes your source; it does not change your configuration. Point the toolchain at the new name:
```ts title="next.config.ts"
export default withPlumeria({}, { styleProp: 'sx' });
```
```ts title="eslint.config.ts"
export default [
{
settings: { plumeria: { styleProp: 'sx' } },
},
];
```
And point the type declaration at the same name — see [classStyle & use()](/docs/api-reference/javascript/use) for the reference that declares the prop.
One practical note: the codemod does not reformat. If the new name is longer than the old one, lines that were near your print width can cross it. Run your formatter afterwards.
## It is not only for migrations [#it-is-not-only-for-migrations]
The names are arguments, so the transform is not tied to any particular release. Renaming the prop to whatever suits your codebase is the same command:
```sh title="Terminal"
npx @plumeria/codemod rename-prop styleName classStyle
npx @plumeria/codemod rename-prop classStyle sx
```
---
# API Reference
Source: https://plumeria.dev/docs/api-reference
Plumeria provides a set of powerful APIs and plugins to help you build type-safe, performant styles.
## JavaScript API [#javascript-api]
The core JavaScript APIs for defining styles, themes, and animations.
## Plugins [#plugins]
Integrate Plumeria with your favorite tools and frameworks.
## Tools [#tools]
Command line tools for maintaining a Plumeria codebase.
---
# Editor integration
Source: https://plumeria.dev/docs/getting-started/editor-integration
## className snippet for style macro [#classname-snippet-for-style-macro]
In VScode create a shortcut for the style.use() object expansion in className.\
`shift + command + p >`\
`select >` Snippets: Configure Snippets\
`select >` typescriptreact.json (TypeScript JSX) or javascriptreact.json (JavaScript JSX)
```json title="typescriptreact.json"
{
"classStyle": {
"prefix": "class",
"body": "classStyle={$1}",
"description": "Plumeria classStyle",
}
}
```
This will make the snippet appear at the top.
```json title="settings.json"
{
// ...other settings
"editor.snippetSuggestions": "top",
// ...
}
```
### Macro Usage [#macro-usage]
```jsx
-> Enter ->
```
## ESLint [#eslint]
It is the plumeria [Linter `@plumeria/eslint-plugin` package](https://www.npmjs.com/package/@plumeria/eslint-plugin)\
The eslint-plugin `plumerialint` command is integrated with `oxlint`.
```sh title="Terminal"
npm i --save-dev @plumeria/eslint-plugin oxlint
```
```sh title="Terminal"
pnpm i --save-dev @plumeria/eslint-plugin oxlint
```
```sh title="Terminal"
yarn add -D @plumeria/eslint-plugin oxlint
```
By incorporating it into your build pipeline, you can proactively detect style warnings and errors and immediately terminate the build midway: `plumerialint`.
```json title="package.json"
"build": "plumerialint && css && next build",
```
```js title="eslint.config.ts"
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import { defineConfig } from 'eslint/config';
import plumeria from '@plumeria/eslint-plugin';
export default defineConfig(
eslint.configs.recommended,
tseslint.configs.recommended,
plumeria.configs.recommended, // [!code ++]
{
files: ['**/*.{ts,tsx}'],
}
);
```
```js title="eslint.config.js"
const eslint = require('@eslint/js');
const tseslint = require('typescript-eslint');
const { defineConfig } = require('eslint/config');
const { plumeria } = require('@plumeria/eslint-plugin');
module.exports = defineConfig(
eslint.configs.recommended,
tseslint.configs.recommended,
plumeria.flatConfigs.recommended, // [!code ++]
{
files: ['**/*.{ts,tsx}'],
}
);
```
```json title=".eslintrc.json"
{
"plugins": ["@plumeria"],
"extends": ["plugin:@plumeria/recommended"],
"rules": {}
}
```
You can jump to definitions from styles even if they are not written in JSX `command or Alt + click`.
---
# Getting started
Source: https://plumeria.dev/docs/getting-started
If you're new to Plumeria, you can follow these steps to get started.
## Install Plumeria [#install-plumeria]
```bash title="Terminal"
npm install -D @plumeria/core
```
```bash title="Terminal"
yarn add -D @plumeria/core
```
```bash title="Terminal"
pnpm add -D @plumeria/core
```
To learn more about installing `@plumeria`, see the [installation guide](/docs/getting-started/installation).
## Choose your learning path [#choose-your-learning-path]
---
# Installation
Source: https://plumeria.dev/docs/getting-started/installation
## Installing `core` [#installing-core]
The core package is type definitions only. bundle and runtime are not included.\
Skip to your bundler: [Next.js](/docs/getting-started/installation#nextjs) / [Vite](/docs/getting-started/installation#vite)
{/* prettier-ignore */}
```sh title="Terminal"
npm i -D @plumeria/core
```
{/* prettier-ignore */}
```sh title="Terminal"
yarn add -D @plumeria/core
```
{/* prettier-ignore */}
```sh title="Terminal"
pnpm i -D @plumeria/core
```
## ESLint Setup [#eslint-setup]
By implementing `ESLint`, you can automatically organize the order of CSS properties and format line breaks, creating a more efficient environment than manual maintenance.
The plumerialint command installs [`@plumeria/eslint-plugin`](/docs/getting-started/editor-integration).\
Setting `languageOptions.parserOptions.projectService` to `true` enables `"@plumeria/no-invalid-selector-nesting"` by utilizing TypeScript type information.
```ts
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import { defineConfig } from 'eslint/config';
import { plumeria } from '@plumeria/eslint-plugin';
export default defineConfig(
eslint.configs.recommended,
tseslint.configs.recommended,
plumeria.flatConfigs.recommended,
{
files: ['**/*.{ts,tsx}'],
languageOptions: {
parserOptions: { projectService: true },
},
}
);
```
`oxlint` is required to run the `plumerialint` command.\
Integrating `plumerialint` into your build pipeline allows for proactive error detection.
```sh
pnpm i -D oxlint rimraf
```
For `Next`, we recommend running rimraf to clean up the cache to accommodate the differences between versions:
```json title="package.json"
"scripts": {
"predev": "rimraf .next",
"prebuild": "rimraf .next",
"dev": "next dev",
"build": "plumerialint -- next build",
"start": "next start",
},
```
```json title="package.json"
"scripts": {
"dev": "vite",
"build": "plumerialint -- tsc -b && vite build",
},
```
## Integration [#integration]
## Next.js [#nextjs]
{/* prettier-ignore */}
```sh title="Terminal"
npm i -D @plumeria/next-plugin
```
```sh title="Terminal"
yarn add -D @plumeria/next-plugin
```
```sh title="Terminal"
pnpm add -D @plumeria/next-plugin
```
### plumeria.d.ts [#plumeriadts]
`@plumeria/core` ships no prop declaration of its own, so name the styling prop your project compiles with.
```ts title="plumeria.d.ts"
/* application root */
///
```
### next.config.ts [#nextconfigts]
It works with both Turbopack and Webpack.
```ts title="next.config.ts"
import type { NextConfig } from "next";
import { withPlumeria } from "@plumeria/next-plugin";
const nextConfig: NextConfig = {
/* config options here */
};
export default withPlumeria(nextConfig);
```
***
## Vite [#vite]
```sh title="Terminal"
npm i -D @plumeria/unplugin
```
```sh title="Terminal"
yarn add -D @plumeria/unplugin
```
```sh title="Terminal"
pnpm add -D @plumeria/unplugin
```
### vite.config [#viteconfig]
```ts title="vite.config.ts"
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import plumeria from '@plumeria/unplugin';
export default defineConfig({
plugins: [react(), plumeria.vite()],
});
```
---
# Selector rules
Source: https://plumeria.dev/docs/getting-started/selector-rules
## Nesting [#nesting]
Pseudo-elements(class) and media nesting rules.\
Queries(media・container) can only be pseudo nest once, but pseudo cannot nest queries.
```ts title="TypeScript" {1}
import * as css from '@plumeria/core';
const styles = css.create({
list: {
'@media (max-width: 768px)': { // [!code ++]
':hover': { // [!code ++]
listStyleType: 'none', // [!code ++]
}, // [!code ++]
}, // [!code ++]
},
});
```
***
Unacceptable examples:
```ts title="TypeScript" {1}
import * as css from '@plumeria/core';
const styles = css.create({
list: {
':hover': {
'@media (max-width: 768px)': { // [!code --]
background: 'skyblue', // [!code --]
}, // [!code --]
},
},
});
```
## Reason [#reason]
This design intentionally avoids reverse nesting.
* Type system limitations (recursive types)
* Avoiding reverse nesting complexity
* Ensuring consistent nesting patterns
---
# Bun
Source: https://plumeria.dev/docs/integration/bun
[`examples/bun`](https://github.com/zss-in-js/plumeria/tree/main/examples/example-bun) shows how to use [`@plumeria/unplugin`](https://www.npmjs.com/package/@plumeria/unplugin) in bun.\
The unplugin compiles Plumeria styles into the CSS asset produced by bun.
## Installation [#installation]
```sh title="Terminal"
npm i -D @plumeria/unplugin
```
```sh title="Terminal"
yarn add -D @plumeria/unplugin
```
```sh title="Terminal"
pnpm i -D @plumeria/unplugin
```
## Usage [#usage]
```ts title="scripts/build.js"
import plumeria from '@plumeria/unplugin';
await Bun.build({
entrypoints: ['./src/index.tsx'],
outdir: './dist',
plugins: [plumeria.bun()],
});
```
---
# Component Library
Source: https://plumeria.dev/docs/integration/component-library
A component library built with Plumeria ships already compiled. The class names sit
in the JavaScript and the rules sit in a stylesheet, so the people installing it need
no bundler plugin, no PostCSS step, and no dependency on Plumeria.
Every bundler in this section produces that dist. [esbuild](/docs/integration/esbuild)
is the fastest of them, so it is the one shown here.
## What ends up in dist [#what-ends-up-in-dist]
Given a component like this:
```tsx title="src/index.tsx"
import * as css from '@plumeria/core';
const styles = css.create({
card: { display: 'flex', padding: '16px', borderRadius: '8px' },
title: { fontSize: '18px', fontWeight: 'bold' },
});
export const Card = ({ title }: { title: string }) => (
{title}
);
```
the build emits this:
```js title="dist/index.js"
import './index.css';
import { jsx } from 'react/jsx-runtime';
var Card = ({ title }) =>
jsx('div', {
className: 'xxp6epoh xqqbxt1d xyqr9vb7',
children: jsx('h2', { className: 'xpdfgu2b xybc5i37', children: title }),
});
export { Card };
```
```css title="dist/index.css"
.xxp6epoh { display: flex; }
.xqqbxt1d { padding: 16px; }
.xyqr9vb7 { border-radius: 8px; }
.xpdfgu2b:not(#\#) { font-size: 18px; }
.xybc5i37:not(#\#) { font-weight: 700; }
```
The `css.create` call and the `@plumeria/core` import are gone. What is left is a
string literal per element and a flat list of rules — nothing that merges objects or
builds class names while your users render.
## Build [#build]
Extracting CSS means the bundler takes the import out of the JavaScript, which is
right for an app — the HTML links the stylesheet — and wrong for a library, where
there is no HTML. Point the built JavaScript back at its stylesheet and your users
inherit it by importing the component.
```js title="build.js"
import esbuild from 'esbuild';
import fs from 'node:fs';
import plumeria from '@plumeria/unplugin';
await esbuild.build({
entryPoints: ['src/index.tsx'],
bundle: true,
outfile: 'dist/index.js',
external: ['react', 'react/jsx-runtime'],
plugins: [plumeria.esbuild({ include: /\.[jt]sx?$/ })],
loader: { '.tsx': 'tsx', '.ts': 'ts' },
format: 'esm',
jsx: 'automatic',
});
fs.writeFileSync(
'dist/index.js',
`import './index.css';\n${fs.readFileSync('dist/index.js', 'utf-8')}`,
);
```
**Build tools — esbuild, Bun, Rollup, Rolldown.** They emit the files you asked
for and stop. The stylesheet is extracted and left unattached, so the line above
is what connects it. This is the usual path for shipping a library.
**Bundlers — Vite, Webpack, Rspack, Farm.** They own the whole graph, so they
follow the import and wire the stylesheet in themselves — into the HTML they
emit, or into the JavaScript. Either way you write nothing. Point one at a
library, where there is no application to wire, and the line is needed again.
## Publish [#publish]
Plumeria belongs in `devDependencies`. It runs while you build and has nothing to do
at your users' runtime.
```json title="package.json"
{
"name": "your-lib",
"type": "module",
"files": ["dist"],
"exports": {
".": "./dist/index.js"
},
"peerDependencies": {
"react": ">=18"
},
"devDependencies": {
"@plumeria/core": "^18.1.2",
"@plumeria/unplugin": "^18.1.2"
}
}
```
## What your users write [#what-your-users-write]
```tsx title="app.tsx"
import { Card } from 'your-lib';
export const App = () => ;
```
That is the whole integration. The component brings its own stylesheet, their
bundler config is untouched, and their `package.json` never names `@plumeria/*`,
whatever they build with.
---
# esbuild
Source: https://plumeria.dev/docs/integration/esbuild
[`examples/esbuild`](https://github.com/zss-in-js/plumeria/tree/main/examples/example-esbuild) shows how to use [`@plumeria/unplugin`](https://www.npmjs.com/package/@plumeria/unplugin) in esbuild.\
The unplugin compiles Plumeria styles into the CSS asset produced by esbuild.
## Installation [#installation]
```sh title="Terminal"
npm i -D @plumeria/unplugin
```
```sh title="Terminal"
yarn add -D @plumeria/unplugin
```
```sh title="Terminal"
pnpm i -D @plumeria/unplugin
```
## Usage [#usage]
```ts title="scripts/build.js"
import esbuild from 'esbuild';
import plumeria from '@plumeria/unplugin';
esbuild
.build({
entryPoints: ['src/index.tsx'],
bundle: true,
outfile: 'dist/bundle.js',
plugins: [
plumeria.esbuild({
include: /\.[jt]sx?$/,
}),
],
loader: {
'.tsx': 'tsx',
'.ts': 'ts',
},
format: 'esm',
minify: false,
})
.catch(() => process.exit(1));
```
---
# Farm
Source: https://plumeria.dev/docs/integration/farm
[`examples/farm`](https://github.com/zss-in-js/plumeria/tree/main/examples/example-farm) shows how to use [`@plumeria/unplugin`](https://www.npmjs.com/package/@plumeria/unplugin) in Farm.\
It compiles styles into CSS via AST parsing, with HMR support.
## Installation [#installation]
```sh title="Terminal"
npm i -D @plumeria/unplugin
```
```sh title="Terminal"
yarn add -D @plumeria/unplugin
```
```sh title="Terminal"
pnpm i -D @plumeria/unplugin
```
## Usage [#usage]
```ts title="farm.config.ts"
import { defineConfig } from '@farmfe/core';
import plumeria from '@plumeria/unplugin';
export default defineConfig({
plugins: [
plumeria.farm({
devEmitToDisk: true,
}),
],
});
```
---
# Frameworks
Source: https://plumeria.dev/docs/integration/frameworks
Plumeria compiles the `css` calls, not the component. Any framework Vite can build reaches it through the same [`@plumeria/unplugin`](/docs/integration/vite) entry — put `plumeria.vite()` in `plugins` beside the framework plugin.
```ts title="vite.config.ts"
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import plumeria from '@plumeria/unplugin';
export default defineConfig({
plugins: [vue(), plumeria.vite()],
});
```
That file is the whole integration. [`examples/vite-vue-ts`](https://github.com/zss-in-js/plumeria/tree/main/examples/example-vite-vue-ts), [`examples/vite-svelte-ts`](https://github.com/zss-in-js/plumeria/tree/main/examples/example-vite-svelte-ts), [`examples/vite-solid`](https://github.com/zss-in-js/plumeria/tree/main/examples/example-vite-solid), [`examples/vite-preact`](https://github.com/zss-in-js/plumeria/tree/main/examples/example-vite-preact), [`examples/vite-qwik`](https://github.com/zss-in-js/plumeria/tree/main/examples/example-vite-qwik), [`examples/react-router`](https://github.com/zss-in-js/plumeria/tree/main/examples/example-react-router) and [`examples/storybook`](https://github.com/zss-in-js/plumeria/tree/main/examples/example-storybook) are that file with the framework plugin swapped.
## The styling prop [#the-styling-prop]
`@plumeria/core/class-style` declares `classStyle` on React's `HTMLAttributes` and `SVGAttributes`, so anything rendering through the React namespace needs one line:
```ts title="plumeria.d.ts"
///
```
A framework that keeps its JSX types in its own module cannot be reached that way. Module augmentation resolves the package from the file that writes it, and `@plumeria/core` does not depend on your framework, so the declaration has to live in your project:
```ts title="types.d.ts"
import 'preact';
import type { Style } from '@plumeria/core';
declare module 'preact' {
namespace JSX {
interface HTMLAttributes {
classStyle?: Style;
}
interface SVGAttributes {
classStyle?: Style;
}
}
}
```
```ts title="types.d.ts"
import '@builder.io/qwik';
import type { Style } from '@plumeria/core';
declare module '@builder.io/qwik' {
interface HTMLAttributes extends JSX.HTMLAttributes {
classStyle?: Style;
}
interface SVGAttributes extends JSX.SVGAttributes {
classStyle?: Style;
}
}
```
Declare `SVGAttributes` even where the framework has it extend `HTMLAttributes` and one interface would do — Qwik's extends `AriaAttributes` instead, and an `