API ReferenceJavaScript API

css.create

export type create = <const T extends Record<string, CreateStyleValue>>(rule: T)=> CreateReturnType<T>;
export const create: create;

create defines css styles using plain JavaScript style objects.
type-safe・autocomplete・lintable

Only the styles you use in classStyle will be ondemand compiled.

Example

TypeScript
import * as css from '@plumeria/core'

const styles = css.create({
  container: {
    display: 'flex',
    justifyContent: 'space-between',
  },
  text: {
    fontSize: '14px',
    textDecoration: 'none',
  },
})

export default function App() {
  return (
    <div
      classStyle={[
        styles.container,
        styles.text
      ]}
    />
  );
}

The call is cleaned up by the bundler.

Compiled:

<div className="xxp6epoh xwbwwha1 xbief8v4 xzllkng3" />

Generated CSS:

.xxp6epoh {
  display: flex;
}
.xwbwwha1 {
  justify-content: space-between;
}
.xbief8v4:not(#\#) {
  font-size: 14px;
}
.xzllkng3 {
  text-decoration: none;
}

Compiles each style CSS property into a unique, atomic, and hashed class name. This prevents style collisions and maximizes reusability.

Dynamic values

You can use functions to define dynamic styles. These are processed as dynamic values at build time.

const styles = css.create({
  palette: (color: string) => ({
    backgroundColor: color,
  }),
});

export const Color = ({ color }) => {
  return (
    <span classStyle={styles.palette(color)}>Dynamic Color</span>
  )
}

Variants bracket notation

You can resolve dynamic variant styles using standard bracket notation (e.g., variantStyles[size]). Plumeria's compiler automatically extracts these variants and creates static lookup tables at build time.

import * as css from '@plumeria/core';

const styles = css.create({
  base: {
    display: 'inline-flex',
    borderRadius: '4px',
    backgroundColor: 'blue',
    color: 'white',
  }
});

const sizeStyles = css.create({
  small: {
    fontSize: '12px',
    padding: '4px 8px',
  },
  large: {
    fontSize: '18px',
    padding: '12px 24px',
  }
});

type Size = 'small' | 'large';
// can also use the type exhaustively via typeof sizeStyles

export const Button = ({ size }: { size: Size }) => {
  return (
    <button classStyle={[styles.base, sizeStyles[size]]}>Button</button>
  );
};

Function keys are ignored

Function keys (dynamic styles) within the style object are ignored during bracket notation lookup table compilation.

The last source wins

An element takes a list, and a property set by more than one entry keeps the value from the one written last. That is what lets a variant override the base above.

The rule holds whatever the entries are — a plain style, a condition, a bracket group. Swapping two of them swaps which one applies:

const styles = css.create({
  base: { color: 'white' },
  muted: { color: 'slategray' },
});

const toneStyles = css.create({
  brand: { color: 'royalblue' },
  danger: { color: 'crimson' },
});

// muted wins — it is written last
<button classStyle={[styles.base, toneStyles[tone], isMuted && styles.muted]} />

// toneStyles wins — now it is
<button classStyle={[styles.base, isMuted && styles.muted, toneStyles[tone]]} />

All three set color here. Entries that touch different properties never compete, so their order does not matter — [styles.base, sizeStyles[size]] above reads the same either way.

Merging goes property by property, and that includes nested blocks. Overriding a :hover colour leaves the rest of the :hover block in place.

After 18.1.1

The order holds across every kind of entry. Earlier releases resolved entries of different kinds by a fixed precedence, so an entry written later could be dropped.

Conditional Expressions with Bracket Notation

The condition can sit inside the brackets or around them. Both compile to a single lookup:

<button classStyle={sizeStyles[isLarge ? keyA : keyB]} />
<button classStyle={isLarge ? sizeStyles[keyA] : sizeStyles[keyB]} />
<button classStyle={isActive && sizeStyles[key]} />

Compiled:

({"small":"xhrr6ses x1v10r0u","large":"xpdfgu2b xj7b4jff"}[isLarge ? keyA : keyB] || "")

The branches of a condition are mutually exclusive — only one of them ever applies — so the compiler folds the condition into the key expression instead of expanding a combination per branch.

That leaves only two things worth knowing:

Written asLookup holds
sizeStyles[isLarge ? keyA : keyB]the keys of sizeStyles
isLarge ? sizeStyles[keyA] : sizeStyles[keyB]the same
isLarge ? sizeStyles[keyA] : themeStyles[keyB]the keys of both objects
isLarge ? sizeStyles.large : sizeStyles.smallnothing — see below

Reaching into two different objects is fine; the table simply carries both key sets.

When both keys are literals, read the property directly. A condition between two styles the compiler already knows needs no table at all:

- <button classStyle={sizeStyles[isLarge ? 'large' : 'small']} />
+ <button classStyle={isLarge ? sizeStyles.large : sizeStyles.small} />

Compiled:

(isLarge ? "xpdfgu2b xj7b4jff" : "xhrr6ses x1v10r0u")

After 18.1

Both placements compile. Earlier releases rejected a condition placed around the brackets, so it had to be moved inside — that workaround is no longer needed.

Optimization Tip: Keep Common Properties in Base Styles

Properties that do not vary belong in a base style — styles.base above — rather than repeated across every key of a variant object.

Anything the variants do not compete for stays outside the decision table, so it is emitted once instead of being multiplied into every combination.

Zero-Runtime logic

Variants, dynamic props and conditional expressions are all resolved at compile time. Nothing merges objects or concatenates class strings in the browser, as many CSS-in-JS libraries do at every render.

1. Minimizing Combinations via Partial Cartesian Products

The compiler builds a property conflict graph across the styles an element can receive, then expands only the axes that actually collide.

  • Independent axes — a size and a theme object touching entirely different properties — stay separate lookups. Two axes of three options cost 3 + 3 entries.
  • Conflicting axes — two objects that both set color, say — are grouped, and only that group becomes a combined decision table. The same two axes now cost 3 × 3.
  • Mutually exclusive branches — the arms of a ?: or && — are never a product at all. Only one arm applies, so they share one table keyed by which arm won.

The output is therefore bound by the element's local axes of conflict, not by how many styles are in play. Stacking variants that never touch the same property stays additive however many you add.

2. Constant O(1)O(1) Runtime Cost

Whatever the table grows to is paid at build time. At runtime the browser reads a single entry from a flat, fully pre-computed object in O(1)O(1) — no object allocation, no string building, and nothing for the garbage collector to reclaim.

Atomic static API

Unique atom classes are reused without duplication, keeping the total number of class names constant.
And all imports are stripped away by the compiler.

On this page