Breaking changes planned for the next major version of Pluma.
This page records breaking changes that no codemod can find for you, so they need a human read before the next major lands. The full record of what v5 changes, and the codemods that cover most of it, is in Migrating to v5.
| Change | Affected | Codemod |
|---|---|---|
Badge gains a variant prop | Any component that wraps Badge and declares its own variant prop | — |
pipelinesTheme is removed | Any app passing pipelinesTheme to PlumaProvider | — |
| Component tokens are removed | Any app setting a --pluma-<component>-* custom property, or reading theme.componentTokens | — |
| Selection controls leave the action fill | Any app painting its own checked, on or selected control with color.scope-action-solid-* | — |
| State classes are removed | Any app styling or querying a Button, Link, PlainButton, TextField, Select, Tabs tab, Table header cell, Calendar day or DatePicker preset by its state class | — |
| SideNav state classes are removed | Any app targeting pluma-c-side-nav-collapsed or pluma-c-side-nav-expanded | — |
Button, Link, PlainButton, TextField, Select, Tabs, TableTh, Calendar and DatePicker now style their runtime state from attributes, and their state classes are deprecated. The classes are still emitted on the same elements, but they no longer carry styles of their own, and the next major removes them. This is what replaces each class:
| Component | Element | Deprecated class | Attribute |
|---|---|---|---|
| Button | button | pluma-c-button-is-loading | aria-busy="true" |
| Button | button | pluma-c-button-is-active | data-active="true" |
| Link | link | pluma-c-link-is-loading | aria-busy="true" |
| PlainButton | button | pluma-c-plain-button-is-loading | aria-busy="true" |
| TextField | input wrapper | pluma-c-text-field-input-wrapper-invalid | data-invalid="true" |
| TextField | input wrapper | pluma-c-text-field-input-wrapper-is-active | data-active="true" |
| Select | select button | pluma-c-select-button-is-invalid | aria-invalid="true" |
| Select | select button | pluma-c-select-button-is-placeholder | data-placeholder="true" |
| Tabs | tab | pluma-c-tabs-tab-is-active | aria-selected="true" (aria-current="page" inside TabNav) |
| Tabs | tab wrapper | pluma-c-tabs-tab-wrapper-is-active | :has(> [aria-selected='true']) (:has(> [aria-current='page']) inside TabNav) |
| TableTh | header cell | pluma-c-table-th-is-active | data-active="true" |
| Calendar | day button | pluma-c-calendar-day-is-selected | data-selected="true" |
| Calendar | day button | pluma-c-calendar-day-is-focused | data-focused="true" |
| Calendar | day button | pluma-c-calendar-day-is-disabled | aria-disabled="true" |
| Calendar | day button | pluma-c-calendar-day-is-selection-start | data-range="start" or data-range="single" |
| Calendar | day button | pluma-c-calendar-day-is-selection-end | data-range="end" or data-range="single" |
| Calendar | day button | pluma-c-calendar-day-is-range-selection | data-range |
| DatePicker | preset button | pluma-c-date-picker-preset-is-active | aria-current="true" |
| DatePicker | preset button | pluma-c-date-picker-preset-is-disabled | aria-disabled="true" |
The classNames exports of Link, PlainButton, TextField, Select, Tabs, Table, Calendar and DatePicker carry the same classes as linkIsLoading, plainButtonIsLoading, textFieldInputWrapperInvalid, textFieldInputWrapperIsActive, selectButtonInvalid, selectButtonPlaceholder, tabsTabActive, tabsTabWrapperActive, tableThIsActive, calendarDayIsSelected, calendarDayIsFocused, calendarDayIsDisabled, calendarDayIsSelectionStart, calendarDayIsSelectionEnd, calendarDayIsRangeSelection, datePickerPresetIsActive and datePickerPresetIsDisabled, and those entries go too. Search for both the class strings and those names: no codemod can find a selector built at runtime.
The SideNav root stops setting pluma-c-side-nav-collapsed and pluma-c-side-nav-expanded, and PlumaSideNav.classNames drops sideNavCollapsed and sideNavExpanded.
No codemod covers this. Class names live in stylesheets and string selectors, so search for both names by hand.
color.scope-action-solid-* is now only the fill of a primary action, such as the primary Button. It carries a label, so it is set for that label's contrast at rest, hover and pressed. The chosen state of a form control -- a checked Checkbox, a Radio dot, a Toggle that is on, a Slider's filled track -- reads the new color.scope-control-solid-bg, with -mark for a checkmark or dot. A chosen state that carries a label, like the selected SegmentedControl segment, stays on action-solid. That fill is set to hold 3:1 against the planes it sits on.
action-solid-bg is teal 800 in both themes. control-solid-bg is teal 700 in both themes: teal 800 falls to about 2.1:1 against the dark planes, and one step for the controls keeps them the same teal in light and dark. In light that makes a checked control a step lighter than it was, and a step lighter than the primary Button.
An app that brands a region by setting color.scope-action-solid-* on a wrapper now reaches only the primary Button and the selected SegmentedControl segment there. Set color.scope-control-solid-bg and -mark too, or checked Checkboxes, Radios, Toggles and Sliders stay teal inside the brand region.
An app that paints its own selection control with color.scope-action-solid-* still builds, but in dark its "on" state loses its 3:1 edge against the page. Move it to color.scope-control-solid-*. No codemod can tell a button-like use from a selection-like one, so this needs a human read.
Every --pluma-<component>-* custom property is gone — 457 of them, from --pluma-accordion-bgColor to --pluma-tooltipContent-textColor. They were an intermediate layer: each one was a single alias onto a semantic token, compiled onto the theme container. Components now read the semantic token where they paint.
The layer also could not do the job it looked like it did. A var() inside a custom property is substituted where that property is declared, so --pluma-checkbox-bgColor-checked: var(--pluma-color-scope-accent-solid-bg) was flattened on the theme container, and a scope override set on a wrapper never reached the Checkbox.
Nothing replaces the names one-for-one, so there is no codemod. Reach for one of two things instead:
--pluma-c-* override variable. Every existing one keeps its name, and nineteen components gained one for each color they used to expose through a component token or read straight from the theme — PlumaCheckbox.vars.checkboxBackgroundColor still has that exact key, it just points at the component's own variable now, so setting it recolors that Checkbox and nothing else.color.scope-accent-*, color.surface-*, color.text-*. This now works, which is the point of the change.This closes the limitation the pipelinesTheme entry below used to carry. A wrapper-level color.scope-accent-* override now reaches every Pluma component inside it, rather than only the ones that happened to read the property directly.
PlumaComponentTokens, the componentTokens field on a theme, and the componentOverrides theme option are removed with it. A theme object is now its tokens, tokensMeta, styles, colorScheme, name and version. @customerio/pluma-css's dist/tokens.css drops the matching 457 bare properties (--checkbox-bgColor-checked and friends).
pipelinesTheme is removedThe pipelines brand overlay and its pipelinesTheme export are gone from @customerio/pluma-components/css. v5 ships lightTheme and darkTheme; darkerTheme was already removed in pluma-components 4.0.0.
Unlike defaultTheme, it has no successor, so the theme-tokens-rename codemod leaves it alone: there is no theme to rewrite it to. An app still importing it gets a build error naming the missing export.
The overlay pointed the accent and action scopes at an aqua ramp of its own and touched nothing below the semantic layer. An app that wants that accent back sets the color.scope-accent-* and color.scope-action-* custom properties on a wrapper element, either as themeVars.color keys or as the --pluma-color-scope-accent-* names directly. That reaches every Pluma component inside the wrapper, with nothing to set per component.
variant propBadge takes variant in v5 (solid, subtle, outline), typed as BadgeVariant. A wrapper that already owns a prop by that name and builds its props from Badge's now declares variant twice:
An intersection of two unions with no members in common is never, and a props type with a never member cannot be spread or used as a component's props. The errors do not point at the wrapper. In one app they surfaced three files away as 'VariantLabel' cannot be used as a JSX component and TS2700: Rest types may only be created from object types, neither of which names variant or Badge.
Add 'variant' to the Omit, or rename the wrapper's prop:
No codemod can find this. A codemod sees the call sites of a prop, not the type arithmetic three files upstream of them, and the wrapper's own source is valid TypeScript either way.
This is not specific to Badge. Any new prop on a widely wrapped component breaks every consumer that already owns a prop by that name and derives its props with Omit or an intersection. The consumer's code did not change, the name did not change, and the compiler reports the failure somewhere else.
When you wrap a Pluma component and add props of your own, list every prop you are replacing in the Omit, including the ones the base component does not have yet. When you add a prop to a Pluma component, expect it to land as a major.