Plumeria 18.2
Plumeria v18.2 lifts a small build error that turned out to be holding back a lot.
css.marker and css.extended take a pseudo-class as their second argument. Until now, passing a functional one — :has(), :is(), :not(), :nth-child() — failed the build. The generated variable name carried the argument along with it, so :nth-child(2n) produced --card-nth-child(2n), which is not a valid custom property. The parser stopped there, and the error it printed was a CSS syntax error with no trace of the API that caused it.
The name is now derived from a hash of the id and the pseudo, so the argument never reaches it:
/* before — invalid */
--card-nth-child(2n): 1;
/* 18.2 */
--xzh6qdtt-card-nth-child: 1;The hash also settles two things that were left to chance. An id holding a character that is not identifier-safe no longer breaks the name, and two markers that share an id and differ only inside the parentheses no longer collide.
Generated variable names change in this release. The stylesheet is regenerated in full on every build, so nothing has to be updated on your side — but if you have been reading --card-hover in devtools, it now reads --xxxxxxxx-card-hover.
What it opens up
:has() is the interesting one. A marker normally travels downward: a state on an ancestor reaches its descendants. With :has() the state belongs to a descendant, the marker lands on the element containing it, and the styles land anywhere below that element.
const styles = css.create({
wrap: {
...css.marker('wrap', ':has(a:hover)'),
},
sibling: {
color: 'gray',
[css.extended('wrap', ':has(a:hover)')]: { color: 'red' },
},
});
export const Row = () => (
<div classStyle={styles.wrap}>
<a href="#">hover me</a>
<span classStyle={styles.sibling}>reacts</span>
</div>
);Hovering the link changes the span. In CSS you would write .wrap:has(a:hover) .sibling, which spells out the path between the two elements. The marker spells out nothing: it is linked to extended by an id, so the styles reach every descendant at any depth, and nesting the span one level deeper does not break the pair.
:defined is the quiet one. It matches every standard element, so the marker is permanently on, and what remains is the plain descendant relation with no state involved.
Every combinator, expressed
With functional pseudo-classes available in both places — as the marker's argument, and as a selector key wrapping a combinator — the relations combinators used to carry are all expressible.
| CSS | Plumeria |
|---|---|
.a .b — descendant, on a state | css.marker(id, ':hover') + css.extended |
.a .b — descendant, no state | css.marker(id, ':defined') + css.extended |
.a > .b — child | css.marker(id, ':defined') + css.extended on the child |
.a + .b, .a ~ .b — sibling | css.marker(id, ':defined') + css.extended on the sibling |
.a:hover ~ .b — sibling reacting to a state | css.marker(id, ':has(a:hover)') + css.extended |
.a:has(.b) .c — a descendant's state, back up and down again | css.marker(id, ':has(input:focus)') + css.extended |
Every row on the left names a path. None of the marker rows do — the pair is linked by an id, so the scope is every descendant at any depth, and rearranging the tree between them changes nothing.
Which one to write
There is a second route the table does not show. A combinator is legal inside a functional pseudo-class — no-combinator permits it there — so a relation can also be written as a selector key, anchored on something you write yourself:
const styles = css.create({
cell: { ':is([data-row] > *)': { color: 'red' } },
});The two routes overlap almost entirely, so the question is which to reach for by default. We measured rather than guessed.
Stylesheet bytes, from a production vite build of the same markup:
| Targets under one relation | Selector key | marker / extended |
|---|---|---|
| 1 | 94 B | 147 B |
| 5 | 281 B | 43 B |
One target, and the marker pair costs more — it emits the marker rule and the container query where a selector key emits one rule. Five, and it is a fifth of the size: every extended sharing an id lands in the same @container block, while a selector key repeats its selector for every declaration.
Style recalculation, over a class toggle in headless Chromium, median of 30 runs:
| Elements | Plain CSS | Selector key | marker / extended |
|---|---|---|---|
| 500 | 0.009 ms | 0.006 ms | 0.012 ms |
| 2,000 | 0.008 ms | 0.007 ms | 0.012 ms |
| 8,000 | 0.005 ms | 0.005 ms | 0.012 ms |
One element toggled per run, which is what a hover or a focus is. The marker column is flat across a sixteen-fold increase in page size, and the gap to plain CSS is 0.007 ms — four hundredths of one frame.
The number that is not flat is the one nobody hits: toggling every anchor at once costs 26 ms at 8,000 elements against 7 ms for a selector key. Style queries are re-evaluated per container, so a broad invalidation pays per container. No interface toggles eight thousand components in the same frame, but that is where the difference lives.
So: reach for marker / extended first. It is smaller from the second target onward, its recalculation cost does not grow with the page, and — the reason it exists — it names no path, so nesting the target one level deeper leaves it working. The selector key is the escape hatch, for markup you do not write and therefore cannot attach a style to.
Neither route reintroduces a dependency on DOM shape into the style objects themselves, which is what the combinator ban was protecting in the first place.
Migrating from CSS Modules
The relations above are what made a CSS Modules migration mechanical, so 18.2 ships one:
npx @plumeria/codemod migrate --from css-modulesIt reads every *.module.css, writes a *.styles.ts beside it, and points the consumers at it — the import, the className prop, the class names, and composes, which becomes an array at the call site.
/* Card.module.css */
.base { font-size: 12px }
.card { composes: base; padding: 16px }
.card:hover { background: teal }
.card .card-title { color: red }/* 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' },
},
});The descendant rule is the part that could not be written before this release. .card .card-title has no combinator to translate into, so it becomes a marker on the parent and an extended key on the child — and because both class names appear in the same selector, the transform reads which is which from the stylesheet alone. No markup is inspected.
What it will not convert, it leaves in place and names:
src/Card.module.css
10:1 sibling-combinator .item + .item
A marker carries no order. Write the relation as a selector key.Sibling combinators, :global, and a composes that reaches into another file are reported rather than guessed at. The exit code is 1 while anything remains, so the command composes with a script.
Moving back: exporting to CSS Modules
Adopting a styling engine should not be a one-way door. The codemod runs in reverse just as mechanically. On this documentation site — 25 files using themes, keyframes, marker / extended, aliased imports and several css.create calls per file — it exports 15 stylesheets and reports nothing.
npx @plumeria/codemod migrate --from plumeriaIt exports css.create definitions to standard *.module.css files, points consumers at className, and handles function style keys by passing them through CSS variables:
/* Card.tsx */
- import * as css from '@plumeria/core';
+ import styles from './Card.module.css';
+ export { styles };
- export const styles = css.create({
- card: { padding: 16 },
- size: (width: number) => ({ width }),
- });
export const Card = ({ width }: { width: number }) => (
- <div classStyle={[styles.card, styles.size(width)]} />
+ <div
+ className={[styles.card, styles.size].join(' ')}
+ style={{ ['--styles-size-width' as string]: typeof width === 'number' ? `${width}px` : width }}
+ />
);Global constructs like css.createTheme, css.keyframes, and css.viewTransition are extracted and appended to src/styles/global.css (or styles/global.css) as CSS custom properties and standard @keyframes rules.
Definitions do not have to sit in the file that uses them. Imports are followed through tsconfig.json paths, directory index files, and filenames carrying a dot, so css.createStatic values reached under an alias are inlined into the CSS rather than reported as dynamic. A file that only defines styles is left where it is, exactly as the forward migration leaves the original stylesheet in place, and its consumers are pointed at the generated module instead.
css.marker and css.extended come across as the custom property and the @container style() rule they compile to, and a file holding several css.create calls is written to one stylesheet.
Run eslint --fix with the recommended config first, then start with --dry-run to inspect what would change. Anything with dynamic semantics that cannot be cleanly converted is reported and left untouched.
Plumeria merges composed styles by property name before it emits anything. classStyle={[styles.a, styles.b]} resolves color once — rightmost wins — and a single declaration reaches the element. Not every pair is the array's to settle: where one property covers another, or sits under a condition, rank decides instead. A longhand carries one step more than the shorthand covering it, a declaration under an at-rule one more again, and the array never enters into it.
The export writes one class per css.create key, so a stylesheet has to reach those same answers with the two things it has. It orders the rules so that every call site's array order holds at once, writes a shorthand before the longhand narrowing it, and puts a narrower condition after the one it implies. Where two call sites compose the same pair in opposite orders, no order satisfies both: the one left over carries an extra class holding only the disputed declarations. A composition whose members come from separate modules is folded into a single class, because no declaration order reaches across two stylesheets.
Which is a lot of claiming for something a reader cannot see. So CI exports this site and the end-to-end app, exports them a second time and requires nothing to move, adopts them back, and then opens both the Plumeria build and the exported one in a browser to compare what the two pages actually paint.
Each direction is a fixed point after the first run:
The round trip is not the identity, and CI does not ask it to be. A project that leaves and comes back has its constants folded and its merged classes named, so exporting it again reaches further than the first export did. What has to hold is that nothing new is reported and nothing new fails to compile.
18.2.34 (Aug 25, 2026)
- Fix: a style function with a destructured parameter, called with no argument at all, aborted the build under
@plumeria/unpluginwith an internalTypeErrorrather than compiling.({ tone = 'navy' }) => ({ backgroundColor: tone })reached for asstyles.box()is the same call asstyles.box({}), and now resolves to the same class; a parameter left without a default is reported the way a named call with a missing key already was. The call already worked under@plumeria/turbopack-loader, so this only ever showed up outside Next.
18.2.33 (Aug 25, 2026)
- Feat:
@plumeria/headlessuiaddsLabel,Separator,Avatar(Image,Fallback),Progress(Indicator) andVisuallyHidden, bringing the exported set to twenty-five component groups. All five are the small primitives a form or a profile row is built from, and until now they were the ones you had to reach past the package for.Separatoris the odd one: it already existed as a part insideSelect,DropdownMenu,ContextMenuandMenubar, but not on its own.
18.2.32 (Aug 25, 2026)
- Fix:
@plumeria/headlessuiloads under Node's ESM resolver. The package is"type": "module", but its entry re-exported the components without file extensions — which a bundler accepts and Node does not — soimport('@plumeria/headlessui')failed withERR_MODULE_NOT_FOUNDanywhere it was not being bundled. - Update: the six
Arrowcomponents drop theclassStyledeclaration they carried.classStylealready reaches them through theSVGAttributesaugmentation in@plumeria/core/class-style, the same way it reaches every other component and every SVG element, so the declaration was restating what was in scope. Nothing changes at the call site and the compiled JavaScript is unchanged. - Update:
@plumeria/inspectorno longer depends on@plumeria/headlessui. It never imported it, so installing the inspector pulled the whole Radix tree in for nothing.
18.2.31 (Aug 24, 2026)
- Fix: allow any number as a
linear()easing stop invalidate-values, so generated spring and bounce curves that overshoot past 1 or dip below 0 are no longer reported. Leading-dot numbers such as.5are now reported inlinear()andcubic-bezier(), as they already were everywhere else.
18.2.30 (Aug 24, 2026)
- Fix: accept CSS variables in individual
cubic-bezier()andlinear()arguments, and stoptext-decoration-linefrom accepting arbitrary tokens invalidate-values.
18.2.29 (Aug 23, 2026)
- Fix: support nested CSS variables while rejecting malformed variables and invalid function values in
validate-values.
18.2.28 (Aug 22, 2026)
- Update: the package now says on npm what it does.
migrate --from css-modulesis the reason most people reach for it, and neither the description nor the keywords named CSS Modules at all.
18.2.27 (Aug 22, 2026)
- Update:
migrate --from css-modulesconverts several selector shapes it used to report. A compound class rides on the class written last, a chain of three or more classes becomes one marker per level with each gated by the one above it, a bare tag under a class takes a key of its own that the consumer rewrite attaches to the markup it can see — a child combinator taking a different key from a descendant one, and reaching one level rather than any — and an attribute selector or a:globalancestor becomes an:is()selector key. - Update: the styling prop is written in stylesheet order rather than in the order the class list carried, a
composesexpansion included, because a class list has no order of its own in CSS. Reads of two stylesheets keep the slots they were given, since which of them the bundler puts first is not a fact one file holds. - Fix: the migration no longer writes a call site it cannot answer for. A class a rule was refused for, and a pair whose order Plumeria's rank settles against the one CSS gives it — a declaration under an at-rule, or further down the shorthand graph, outranks one written after it — are reported and the stylesheet is held whole: nothing written and no consumer moved, so a later run converts it once the rule named in the report is dealt with.
18.2.26 (Aug 21, 2026)
- Fix: two components declared in one file that receive a style through the same prop name shared one lookup table, so the second rendered with the first one's keys and reached the element with no class at all. Each component now reads the table it owns, in the bundler plugins and in the generated stylesheet alike.
- Fix: a style function's parameter default was read as neither a default nor a name, so
(c = 'red') => ({ color: c })produced an empty class even when an argument was passed. A default now compiles to the CSS variable's fallback, sobox()andbox('blue')share one class and only the second writes an inline style. - Fix: a style function that takes no parameter resolved to nothing when it was called, so
() => ({ color: 'red' })left the styling prop off the element entirely. It now compiles to the class its static equivalent would. - Fix: a style key that is only digits was dropped without a word.
css.create({ 1: { color: 'red' } })is now reported, the way@plumeria/no-invalid-selectoralready reported it; a quoted'1'and a name carrying digits are unaffected. - Fix: a style key holding a quote or a backslash was written into the run-time lookup table unescaped, which made the compiled module invalid JavaScript.
- Fix:
css.use()with no arguments, andcss.use()on a style that cannot be resolved, survived into the output after the@plumeria/coreimport had already been removed, leaving a module that throws on load. The first compiles to an empty class and the second is reported. - Fix:
css.keyframes()with no argument aborted the transform with an internal error instead of being skipped, the waycreate()andviewTransition()already are. - Update: the style prop table is read by the owning component's key rather than scanned, so a project with many components generates its stylesheet faster — 44ms against 83ms on a 400-component benchmark.
18.2.25 (Aug 19, 2026)
Update dependencies
18.2.24 (Aug 18, 2026)
- Fix:
migrate --from css-modulesoverwrote a*.styles.tsthat was already there without reporting it. A stylesheet whose target exists is now reported astarget-existsand left alone, the way the export already treats an existing*.module.css, and its consumers keep the import they had rather than being pointed at a file that was never written. - Fix:
migrate --from css-modulesleft a style read with a run-time key spelling the stylesheet import it had just renamed, sos[name]survived as an unbound identifier. The read now carries the generated binding. - Fix:
migrate --from plumeriafolded a style key named by a constant only inside a composition array, soclassStyle={styles[VARIANT]}kept the constant while the import that carried it was retired. A lone call site folds the same way.
18.2.23 (Aug 18, 2026)
- Fix:
migrate --from css-modulesdrops the pixel guard the export put around a function style argument, sostyles.box(width)comes back as it was written rather than carrying atypeoftest the custom property no longer needs. Left in, it was wrapped in another guard the next time the project was exported and the file stopped compiling, so a project with a function style in it could be adopted back but never exported again. - The round trip closes: a project exports, adopts back, and exports again, with each direction reaching a fixed point on its second pass.
18.2.22 (Aug 18, 2026)
- Fix:
migrate --from plumeriareproduces the rank a declaration is given in Plumeria — one step per shorthand covering the property, one more under an at-rule — which settles the pair rather than the order it was written in. It orders the rules and the declarations inside them, so a base longhand still outranks a shorthand set under a media query. - Fix:
migrate --from plumeriarecognises two properties that only partly overlap —borderColoragainstborderTop— as sharing ground, so the order the call site composes them in is carried into the stylesheet instead of being read as unrelated. A pair that both covers and partly overlaps keeps both answers, through a class that carries only the crossing declarations. - Fix:
migrate --from plumeriawrites a narrower condition after the one it implies, so a@media (min-width: 900px)rule still wins where a@media (min-width: 600px)rule also matches. - Fix:
migrate --from plumeriafolds a composition whose members come from separate modules into a single class in the first of them. Two stylesheets have no order that reaches both, and a fold puts the members back under one that does. - Fix:
migrate --from plumeriacarries the unit Plumeria would have added into an argument reaching a function style through a custom property, sostyles.box(100)is still100pxafter the export. - Fix:
migrate --from css-modulesfinds a module the same way wherever it looks, so a style read as a value and a function style call are both restored in a file whose stylesheet sits beside it.
18.2.21 (Aug 18, 2026)
- Feat:
migrate --from plumeriaresolves a style read with a key named by a constant —styles[size]wheresizeis aconstholding a string, in the same file or imported from another — to the class it names instead of reporting it, and removes a constant left naming nothing with it. - Fix:
migrate --from css-moduleswrites a restored read with the name the import carries, so a composed array, acss.usecall, and a local holding a style survive a file whose module was renamed on the way back.
18.2.20 (Aug 18, 2026)
- Fix:
migrate --from plumeriareproduces the order aclassStylearray composes in, by ordering the generated rules to satisfy every call site, collapsing an unconditional array into one class thatcomposesits members, and giving a call site that still disagrees an override class carrying only the disputed declarations. - Fix:
migrate --from plumerialeaves a file the plan cannot export in Plumeria together with every file it reads definitions from, so a partial migration no longer leaves source that does not compile. - Fix:
migrate --from plumeriaconverts negative values,css.use, a token read outside a style, a style arriving through a prop, and aclassStyleon an element that already carriesclassName, instead of reporting them or rewriting them by halves. - Fix:
migrate --from plumeriawrites the generated import with the other imports rather than where thecss.createstood, and replaces the block appended toglobal.csson a re-run instead of appending it again. - Fix:
migrate --from css-modulespoints a consumer at where the module actually landed, restores a composed array, acss.usecall, and a function style call, and keeps one name per module when a file reads several. - Fix:
migrate --from plumeriareports a style read with a computed key rather than exporting it, because the class it names is not known until it runs.
18.2.19 (Aug 17, 2026)
- Feat:
npx @plumeria/codemod migrate --from plumeriaexportscss.createdefinitions to CSS Modules, writes global styles (createTheme,keyframes,viewTransition) tosrc/styles/global.css, and rewrites consumers to useclassName. - Fix: import specifiers are resolved through
tsconfig.jsonpaths, directoryindexfiles, and filenames that carry a dot, so a definition reached under an alias is found instead of reported as dynamic. - Feat:
css.createStaticvalues imported from another file are inlined into the generated CSS, and the import is removed only when nothing outside the exported styles still reads it. - Feat:
css.markerandcss.extendedare expanded into the custom property and the@container style()rule they compile to. - Feat: several
css.createcalls in one file are written to one stylesheet, renaming a key an earlier call already claimed and repointing its usages. - Feat: a file that only defines styles is left untouched and its consumers are pointed at the generated
*.module.css, matching how--from css-modulesleaves the original stylesheet in place.
18.2.18 (Aug 16, 2026)
- Update: groundwork for
npx @plumeria/codemod migrate --from plumeria. Note: the command is usable from 18.2.19.
18.2.17 (Aug 16, 2026)
- Update: README.md and documentation describe using
--dry-runto preview a CSS Modules migration without writing files.
18.2.16 (Aug 16, 2026)
- Update: README.md separates the rules enabled by the recommended config from optional property-policy and border-expansion rules.
18.2.15 (Aug 16, 2026)
- Feat: two states that can hold at once, such as
:hoverand:focus, now settle their intersection by composition order. The atom from the style written further right inclassStylereceives one more:not(#\#)and becomes a class of its own, so reversing the array reverses the winner and the module the bundler reached first no longer decides. Only a pair that is otherwise indistinguishable is weighted; a differing at-rule, shorthand depth or selector specificity is left alone, and an explicit compound such as:hover:focusstays above the weighting. - Feat:
no-order-dependent-overlapalso reports two states written in one style that can match at the same time and set the same property, including two states on one pseudo-element such as:hover::beforeand:focus::before. Mutually exclusive states, differing pseudo-elements and pairs already ranked by specificity are left alone. Report only, no suggestion.
18.2.14 (Aug 15, 2026)
- Fix: ESLint rules now check properties inside function style keys, including unused keys, validation, sorting, formatting, and shorthand expansion.
18.2.13 (Aug 15, 2026)
- Fix: a function key on a style imported from another file is compiled instead of silently dropped, in the transform and in the generated CSS alike.
- Fix: an imported style resolves the constants,
createStaticandcreateThemevalues of the file that declares it; both function keys and static keys lost those declarations before.
18.2.12 (Aug 14, 2026)
- Fix:
expand-border-shorthandsexpands avar()or a template-literal expression that stands as one token, assigning it to the component the other tokens leave open; such a value was reported as unsplittable. - Fix: the expansion writes
currentColorfor an omitted color instead ofcurrentcolor, whichvalidate-valuesrejected, andvalidate-valuesnow listscurrentColoras aborderColorkeyword.
18.2.11 (Aug 13, 2026)
- Fix:
no-order-dependent-overlapalso reports two conditions in one style where one provably matches a subset of the other and the narrower one is written first. Only a single numericmin-ormax-range on one axis is compared; the swap is offered as a suggestion. - Fix:
@plumeria/utilsplaces a conditional at-rule after every condition that contains it while optimizing, so the narrower condition wins everywhere both apply. - Fix:
[email protected]reads a condition as the interval it matches, so implication is recognized acrossmin-/max-prefixes, comparison and two-sided forms, andandchains, onwidth,height,inline-sizeandblock-size, for@containeralongside@media. Note: two conditions that overlap without either containing the other, such as600px–900pxagainst700px–1000px, are still decided by source order.
18.2.10 (Aug 13, 2026)
- Feat:
@plumeria/eslint-pluginaddsexpand-border-shorthands, a fixable rule that expands the eleven border bundles (border,borderTop,borderBlock, …) into their width, style and color declarations, writing each component's initial value where one was omitted. A value that cannot be split, such asvar(--edge)orinherit, is reported without a fix. Not part ofrecommended.
18.2.9 (Aug 13, 2026)
- Fix:
@plumeria/compilersorts the file list it globs, so the stylesheet is emitted in the same order on every machine. The scan pass is sorted as well. - Feat:
@plumeria/eslint-pluginaddsno-physical-propertiesandno-logical-properties, which keep a project on one spelling of the properties that carry both a logical and a physical name.{ sizes },falseby default, extends them towidth,height, theirmin/maxforms,overflow-xandoverflow-y. Neither is part ofrecommended.
18.2.8 (Aug 13, 2026)
- Feat:
@plumeria/eslint-pluginaddsno-order-dependent-overlap, which reports two properties written in one style that overlap while neither one outranks the other: one property under its logical and its physical name, or two shorthands that cross, such asborderTopbesideborderBlockWidth. Suggestions only, no autofix.warninrecommended. - Fix:
[email protected]registerscorner-shapeand the eight corner longhands it sets, which carried no depth at all, and exports the shorthand graph the rule ranks pairs from. Note: the corner longhands now receive one:not(#\#)where they received none. Class names are unaffected.
18.2.7 (Aug 12, 2026)
- Fix:
@plumeria/coreexposesoverflowInlineinstead ofoverflowBlockX— the logical counterpart ofoverflow-xisoverflow-inline, and CSS has nooverflow-block-x. - Fix:
[email protected]aligns the shorthand graph with the logical property groups: the physical edges now sit below the axis shorthand that sets them acrossmargin,padding,inset,scroll-margin,scroll-paddingand theborderaxes,overflowandoverscroll-behaviorgain their missing logical longhands, andcontainIntrinsicSize,positionTry,marker,cue,pauseandrestare registered as shorthands. Note: these depths decide how many:not(#\#)selectors an atomic class receives, so the generated stylesheet changes for the properties listed above. Class names are unaffected.
18.2.6 (Aug 12, 2026)
- Fix:
@plumeria/utilsgives a nested selector inside a conditional rule both the nested and the condition depth, so@media { ':hover': ... }outranks a bare':hover'regardless of stylesheet order. Custom properties stay exempt and receive no depth. - Fix:
@plumeria/utilsmoves conditional at-rules (@media,@container,@supports,@layer,@scope) after base rules while optimizing, so a conditional declaration wins over a base declaration of equal specificity.@keyframesis left where it is.
18.2.5 (Aug 12, 2026)
- Fix:
@plumeria/utilspreserves shorthand and longhand declarations as independent atoms, and merges identical selectors and at-rules without reordering them during optimization. - Fix:
@plumeria/turbopack-loaderoptimizes its accumulated development virtual stylesheet, so duplicate selectors and at-rules are merged consistently. - Fix:
[email protected]removes the override-longhand behavior: a longhand written above a shorthand is no longer crushed by it, and declarations merge the same way regardless of order. No effect under the ESLintrecommendedrules. - Fix: logical longhand properties receive specificity from their depth in the shorthand graph —
padding-block-startgets two:not(#\#)selectors in base styles and three inside conditional rules. - Update: base and conditional priority is controlled by specificity instead of moving media queries to the end of the stylesheet, so development and production behave the same.
18.2.4 (Aug 11, 2026)
- Fix: drop the asterisk wildcard from style prop attribution. A prop now counts as applied only inside the component that received it, so a component that merely forwards a same-named prop fails the never-applied check instead of slipping past it.
18.2.3 (Aug 11, 2026)
- Fix: a style prop that is never applied fails the build instead of silently dropping the style.
classStyle={[styles.base, cond && styleArray]}andclassStyle={cond ? styleArray : styles.base}now compile: the styles a prop's call sites pass are carried through the condition, and a closed gate leaves the surrounding styles in place.
18.2.2 (Aug 9, 2026)
- Update: dependencies
- Update: README.md
18.2.1 (Aug 7, 2026)
- Feat: migrate a CSS Modules stylesheet with
npx @plumeria/codemod migrate --from css-modules, which converts the stylesheet, rewrites its consumers, and reports the rules it did not convert
18.2.0 (Aug 7, 2026)
- Feat: accept functional pseudo-classes in
css.marker/css.extended - The generated marker variable now carries a hash, so its name changes
- Note: the marker's css variable is derived from a hash of the id and the pseudo, so the variable name changes in this release
- Note: an
extendedstyle is keyed by the container query that wraps it, so its class name changes with the variable name
Thanks for building with Plumeria! If you have any feedback or questions, please let us know on GitHub Discussions or GitHub Issues.