Plumeria 19.1
Plumeria v19.1 tightens the type constraints v19.0 introduced and adds a type for the prop that takes a single style.
The 18.5 patches spent four releases closing the paths where a style expression compiled to nothing. What they could not change is when you find out. A build error arrives after you have written the file; a type error arrives while you are writing it. That is the move 19.0 made, in four parts:
Styleaccepts only created styles — an inline object is a type error, not a build error.StyleProps,WithoutPropertiesandStaticStyles— a component can state which properties it accepts through a style prop.StaticStylesrejects a dynamic style — the one thingcss.use()cannot carry, said at the call site.validate-at-rules— a lint rule for the one key the type system cannot reach.
19.1 adds a fifth, and makes the first two hold in the cases 19.0 let through:
AtomicStyle— a prop that takes a single style can say what has to be in it, and read it back.
Sections 1 to 4 describe what 19.0 shipped and 19.1 keeps; section 5 and Upgrading are what is new.
1. Style accepts only created styles
- <div classStyle={{ color: 'red' }} />
+ <div classStyle={styles.text} />Style used to be written in terms of CSSProperties, the shape of the objects you hand to css.create. That made an inline object structurally valid, so it type-checked, and the compiler rejected it a step later. The two layers disagreed, and the gap was covered by a lint rule you had to be running.
Style is now written in terms of what css.create returns. Each property of a created style is a class name carrying the property it was generated for, and a plain string is not one of those, so the object literal has nowhere to land:
type Style = StyleList<StyleNamespace>;The type reaches further than the rule it overlaps with. no-inline-object sees the object handed directly to css.use(); it does not see the one nested inside a style list. The type sees both:
css.use({ color: 'red' }); // rule and type
css.use([styles.text, { ':hover': { color: 'blue' } }]); // type only2. A prop can name the properties it accepts
A design system component owns a shape. Letting a caller override its padding is deliberate; letting one override its position is how a card escapes its grid. Until now the type had no way to say which is which, because a compiler can check that a style resolves — it cannot know which overrides you meant to allow.
StyleProps enumerates them:
type TextProps = {
styleArray?: css.StyleProps<'color' | 'fontSize'>;
};WithoutProperties is the inverse, for the common case of protecting a few properties and leaving the rest open:
type PanelProps = {
styleArray?: css.WithoutProperties<'position' | 'top' | 'left'>;
};Both reach into nested blocks. A property cannot slip past the constraint by being written inside a pseudo class or an at-rule:
// Rejected by WithoutProperties<'position'>
<Panel styleArray={styles.hoverPinned} />const styles = css.create({
hoverPinned: {
':hover': {
position: 'fixed',
},
},
});3. StaticStyles rejects a dynamic style
A dynamic style is a style defined as a function. Its values are computed by the caller, so they reach the element as custom properties — and a class name string has no room for a custom property. That is why 18.4.1 made css.use() report one instead of dropping it.
Reporting it is still a build error. A component that renders through css.use() can now say so in its own signature:
type ChipProps = {
styleArray?: css.StaticStyles;
};
function Chip({ styleArray }: ChipProps) {
return <span className={css.use(styles.base, styleArray)} />;
}<Chip styleArray={styles.tinted} /> // ok
<Chip styleArray={styles.sized(120)} /> // Type errorWritten without a type argument, StaticStyles accepts every property and rejects only the dynamic style, which is the shape most components want. A type argument narrows the properties as well, so StaticStyles<'color'> is StyleProps<'color'> without dynamic styles.
A component that applies its styles through classStyle has no such limit and should keep using StyleProps or WithoutProperties.
4. An unrecognised at-rule is reported
A key inside a style object is the one place the type system cannot help. css.create infers its argument through a generic constraint, and a constraint check performs no excess property check, so a key that matches nothing was accepted:
css.create({ box: { '@unknown foo': { position: 'absolute' } } });The compiler emitted nothing for it and raised nothing either. validate-at-rules reports it:
Invalid at-rule: "@unknown foo".It accepts @media, @container, @supports, @layer and @scope, each with a prelude, and reaches into pseudo classes, nested at-rules and dynamic styles. Given TypeScript project information it resolves computed keys as well, the same way no-invalid-selector and validate-pseudos do. It is error in recommended.
The rule is scoped to css.create because that is where the gap is. createTheme takes its selector as a plain parameter rather than a key inside an inferred object, so the type already rejects an unrecognised at-rule there.
5. A component can look inside one style
The four types above all describe a style list. A prop typed with one of them can be handed an array, a conditional or nothing at all, which is what a styling prop wants — and it is also why such a prop cannot be indexed. There is no single style there to read a property off.
AtomicStyle is the other shape. It names one created style by the rule object it was created from, so its type argument is a requirement rather than a permission:
function Glyph({ glyphStyle }: { glyphStyle: css.AtomicStyle<{ color: string }> }) {
return <svg fill="currentColor" className={glyphStyle.color} />;
}<Glyph glyphStyle={styles.tinted} /> // ok
<Glyph glyphStyle={styles.padded} /> // Type error, there is no color to readRequiring the property is what makes it readable. glyphStyle.color is the atomic class name generated for that property, so a component can hand one property of a caller's style to something that takes a className and nothing else.
Because an atomic class name carries the value it was generated for, a literal in the type argument holds the style to it. css.AtomicStyle<{ display: 'flex' }> takes a style that sets display: flex and rejects one that sets display: grid. AtomicDynamicStyle is the same thing for a style defined as a function, once it has been called.
This is the reverse of what StyleProps and WithoutProperties do. Those say which properties a prop will accept; AtomicStyle says which ones have to be there. A styling prop still wants Style or one of its narrowed forms — AtomicStyle is for the prop a component looks inside.
The two names are also what TypeScript writes when it emits a declaration for an exported style, so a package that ships styles now compiles with declaration: true. Before 19.1 that emit failed: the symbol that tags a created style had no name outside the package.
Upgrading
From 18.5. An inline object in a style prop is a type error. It was already a build error, so a codebase that built on 18.5 still builds — the change is that the editor says it first. Move the object into css.create and pass the created style. The prop types are additive: Style still accepts every property, and a component that does not need to constrain anything needs no change.
From 19.0. Three shapes that used to type-check no longer do, all of them things 19.0 set out to reject and missed: the object css.create returns passed whole, an object assembled by hand out of atomic class names, and an object with no style properties at all. Nothing that runs changes — the compiler already refused to resolve any of the three — so the work is in the editor, not in the output. AtomicStyle and AtomicDynamicStyle are additive on top.
@plumeria/codemod knows every one of these types, the two Atomic ones included. The export removes the @plumeria/core import, so a reference left behind has nothing to declare it — a component prop, a type alias, a variable annotation alike. Those modules are now left in place and reported:
component/Badge.tsx
21:16 style-type-reference
`css.StaticStyles` has no CSS Modules equivalent, and the import that
declares it does not survive the export of Badge.tsx.Before, the export rewrote such a component's call sites to className while its declaration still said classStyle, and left the file uncompilable. It now joins dynamic-value and the other reasons the export already leaves styles behind, so a migration that meets one is partial rather than broken. One shape is still outside it: a module that declares nothing but style prop types has no stylesheet to hold the report, and its consumer will not compile after the export. Keeping the prop type in the file that defines the styles avoids it, and the codemod reference says so where the other reports are listed.
Release notes
19.1.0 (Sep 11, 2026)
- Fix: a style type accepts only a namespace
css.createreturned, so the object that holds the styles, and an object that came from anywhere else, are type errors - Fix: key
Markerby the pseudo class it emits, so a style built fromcss.marker()passes a constrained style prop type - Fix: accept an at-rule prelude that follows the keyword without a space, both in the type and in
validate-at-rules - Feat: add
AtomicStyleandAtomicDynamicStyle, which name one created style by the rule object it was created from, so a prop can require a property and pin its value - Fix: a declaration file can name the type of an exported style through those exports, instead of failing on the symbol that tags it
- Fix: the export reports a reference to
AtomicStyleorAtomicDynamicStyleas well, instead of migrating a module that still needs the import
19.0.0 (Sep 9, 2026)
- Feat: add
StylePropsandWithoutPropertiesfor constraining the properties a style prop accepts - Feat: add
StaticStyles, which rejects a dynamic style and narrows properties when given a type argument - Feat: add the
validate-at-ruleslint rule, which reports an unrecognised at-rule insidecss.create() - Feat:
@plumeria/codemodreports a reference to a Plumeria style type instead of removing the import it still needs - Fix:
css.use()declared a rest parameter that was not an array type - Fix: the
classStyleglobal augmentation was missing itsdeclaremodifier - Break:
Styleaccepts only styles created bycss.create; an inline object is a type error
Thanks for building with Plumeria! If you have any feedback or questions, please let us know on GitHub Discussions or GitHub Issues.