How to migrate an app from Pluma v2 to v3 — every breaking change, its codemod, and the recommended order.
This guide walks through migrating an app from Pluma v2 to v3. It covers every breaking change that shipped in the major, the codemod that automates each migration, and the recommended order to run them in. The canonical record of what changed lives in the package changelogs.
| Change | Affected | Codemod |
|---|---|---|
| Long-hand size names removed | size prop on Button, ButtonGroup, ProgressBar, SegmentedControl, Select, TextField, Toggle | size-longhand-rename |
| Short-hand token names removed | s/m/l size tokens; size prop on Text, Heading, Paragraph; the font-size-s/m/l and text-product-ps custom properties | text-size-shorthand-rename, css-token-rename |
*Primitive component renames | PopoverPrimitive, ModalPrimitive, SelectPrimitive | primitive-rename |
| Component removals | ToggleButton, LabelIcon, Label | toggle-button-to-button, label-icon-to-coin, label-to-badge |
| Button variant consolidation | variant prop on Button, ButtonGroup | button-variant-rename |
| Event handler standardization | onChange → onCheckedChange on OptionCard, Radio, Checkbox | event-handler-rename |
| Deprecated props removed | Button.isError, Snackbar.isError, Link.isError, isDanger → isCritical on Button/ConfirmationModal/DropdownMenu.Item/DataTable actions, Banner/Snackbar warning/error variants, Popover.content, DataTable sorting APIs, Badge.isOutline, Badge.isEmphasized, the Banner/Snackbar error custom property names | deprecated-props-removal, label-to-badge, css-token-rename |
| Unsafe section components removed | unsafe_leftSectionComponent / unsafe_rightSectionComponent on TextField; unsafe_leftSectionComponent on Select | — |
| DataTable on TanStack Table v9 | DataTable Ember cell/header components, column definition types, sortingFn, table.getState(), ember-source peer range | data-table-tanstack-v9 |
| Badge default element | Badge renders a span instead of a div | — |
| Legacy compatibility styles removed | unsafe_isLegacy on Button, DatePicker, Link, Select, TextArea, TextField, ToggleButton; unsafe_useLegacy* PlumaProvider flags | — |
| Icon sizing + deprecations | All icon usage | icon-deprecations |
Test helper get removed | PlumaTestHelpers | test-helper-get-rename |
Most changes ship with a codemod — see the Pluma CLI codemods for details and running instructions. A couple of migrations are manual: removing the legacy compatibility styles (drop the prop or flag; the component already renders in the target style) and updating the DataTable sorting APIs.
Long hand size prop values have been removed in favor of shorter, more consistent alternatives:
medium → mdsmall → smlarge → lgAffected Components:
Button - size propButtonGroup - size propProgressBar - size propSegmentedControl - size propSelect - size propTextField - size propToggle - size propThe size-longhand-rename codemod renames these props across .tsx/.jsx and .gts/.gjs/.hbs files.
Short hand size token names are expanded to two letters for clarity:
s → smm → mdl → lgThis affects the font-size-* scale (font-size-s/m/l → font-size-sm/md/lg) and the product-ps text style ( → product-psm). On components, the Text, Heading, and Paragraph size prop values change accordingly (size="s" → size="sm", etc.). Extended sizes (xs, xl, xxs, xxl, xxxl) are unchanged.
The text-size-shorthand-rename codemod renames these props across .tsx/.jsx and .gts/.gjs/.hbs files.
The raw CSS custom properties are migrated by the css-token-rename codemod, which covers both the --pluma- prefixed spelling @customerio/pluma-components/css ships and the bare spelling @customerio/pluma-css ships. Run it before the 5.0 theme-tokens-rename codemod, whose table starts from the 3.0 vocabulary.
*Primitive ComponentsThe *Primitive components are renamed for clarity. The genuinely unstyled primitives adopt the Plain* prefix; SelectPrimitive — which is really the trigger-less select behavior layer rather than an unstyled primitive — becomes SelectController:
PopoverPrimitive → PlainPopoverModalPrimitive → PlainModalSelectPrimitive → SelectControllerThis affects all related subcomponents, prop types, context, and style exports as well (e.g. PopoverPrimitiveContent → PlainPopoverContent), and moves the per-component subpaths from @customerio/pluma-components/{react,ember}/{popover,modal}-primitive to .../plain-{popover,modal}. SelectPrimitive becomes a standalone SelectController component with its own @customerio/pluma-components/{react,ember}/select-controller subpath (it previously shipped under the select subpath). Run the primitive-rename codemod to migrate imports, tags, and type references across React and Ember.
The following components have been removed:
ToggleButton — migrate to Button, whose prop surface (including isActive) is a superset. Run the toggle-button-to-button codemod.LabelIcon — migrate to Coin with a child Icon. Run the label-icon-to-coin codemod.Label — migrate to Badge with the color prop, which supports every Label color and renders identically. A bare Label becomes color="grey" (Badge defaults to the neutral color) and color="outline" becomes color="neutral-outline". Run the label-to-badge codemod.Badge renders a <span> instead of a <div> by default. A badge is text-level UI, and a <span> is valid in phrasing contexts (paragraphs, headings, links, buttons) where a <div> is not — this also matches the element the removed Label component rendered, so Labels migrated into text flow are valid automatically.
The change is visually inert (Badge forces display: inline-flex either way), but:
HTMLDivElement to HTMLSpanElement (React ref types and the Ember Element signature) — TypeScript will surface every affected site.div need updating.as="div" to retain the previous element where it matters.Button variants have been consolidated from four to three. The current secondary variant is removed; the remaining variants shift down a tier so the visual styles each variant produces stay continuous:
| Current variant | After the major |
|---|---|
primary | primary (unchanged) |
secondary | removed |
tertiary | secondary (now the default-looking, less-prominent button) |
subtle | tertiary (now the most muted variant) |
This change has shipped in the current major. The two intermediate flags
(unsafe_withSoftDeprecatedSecondaryVariant and unsafe_withMigratedButtonVariants)
that were available on the previous major have been removed — variants now render in
their consolidated form natively.
To migrate an app off the previous major without visual regressions:
unsafe_withMigratedButtonVariants on PlumaProvider's componentConfig.PlumaButton (or per-button), then run the button-variant-rename codemod. The codemod renames tertiary → secondary and subtle → tertiary across .tsx/.jsx (jscodeshift) and .gts/.gjs/.hbs (Glimmer AST) files. With the flag on, the renamed source renders identically to before — no visual regressions.See the codemod README for the full migration order, running instructions, and limitations.
Several components have migrated to more consistent event handler naming:
Change onChange to onCheckedChange:
OptionCard.onChange → OptionCard.onCheckedChangeRadio.onChange → Radio.onCheckedChangeCheckbox.onChange → Checkbox.onCheckedChangeRun the event-handler-rename codemod to rename these handlers across React and Ember.
The deprecated-props-removal codemod migrates the Button, ConfirmationModal, DropdownMenu.Item, Link, Snackbar, Popover, and Banner props below; the DataTable sorting changes are manual.
isError prop (use isCritical instead)isDanger prop to isCriticalisError prop to isCritical (same critical color treatment, new name)isDanger prop to isCritical. The codemod covers JSX attributes and Ember args (Button, DropdownMenu.Item); isDanger used as an object key — useConfirmationModal({ isDanger: true }) / ConfirmationModal data configs (the only way that component receives it) and DataTable action configs — must be renamed by hand (TypeScript flags every affected site).isError prop (use variant="critical" instead)error variant to criticalwarning variant (use caution instead)error variant to criticalcontent prop (use description instead)isEmphasized prop (use color="accent" instead)isOutline prop (use color="neutral-outline" instead, or color="accent-outline" where it was combined with isEmphasized). Run the label-to-badge codemod.{ id, desc } object form for sorting (use sorting={{ value: { id, desc } }})onSortingChange prop (use onChange in the sorting object instead)The deprecated form sorted rows client-side. The new sorting object defaults to manual (server-side) sorting when both value and onChange are provided, so the table no longer reorders rows itself. To keep in-table (client-side) sorting while migrating, add isManual: false:
Leave isManual off (it defaults to true) if you sort the data yourself, e.g. server-side.
TextField.unsafe_leftSectionComponent (use elementBefore instead)TextField.unsafe_rightSectionComponent (use elementAfter instead)Select.unsafe_leftSectionComponent (use elementBefore instead)elementBefore and elementAfter take the same content and render in the same places, so the migration is a rename. In Ember they can additionally be filled with an :elementBefore / :elementAfter named block, which the unsafe_ props never supported.
NumberField and Combobox used the trailing slot internally for their own controls. They now compose: the component's controls render after whatever you put in elementAfter, so both appear together.
DataTable is now built on TanStack Table v9, and its Ember integration comes from
TanStack's own @tanstack/ember-table package instead of Pluma's hand-rolled one.
The data-table-tanstack-v9
codemod handles the mechanical parts.
The @customerio/pluma-components/ember/tanstack/table entry point is removed, along
with flexRenderWrapper. Import flexRenderComponent from @tanstack/ember-table
instead.
TanStack v9 also splits what Pluma passed as a single @props argument into two: the
cell or header context arrives as @ctx, and whatever you passed to
flexRenderComponent arrives as @options.
A bare component class can no longer be passed directly as a cell or header value —
TanStack calls a function-valued definition to compute the cell, and a component class
is a function. Wrap it in flexRenderComponent(...).
FlexRender is replaced by the FlexRenderCell, FlexRenderHeader, and
FlexRenderFooter components from @tanstack/ember-table.
The ember-source peer range is now >=5.8.0, which @tanstack/ember-table requires.
TanStack's types gained a leading features type parameter. DataTableFeatures is
exported from both entry points for this:
The same applies to Table, Row, Cell, Column, and ColumnDef.
sortingFn on a column definition is now sortFn. Leaving the old name behind does
not error — the column just stops sorting — so this one is worth checking for.table.getState() is removed. Read the whole snapshot with table.store.state, or a
single slice with table.atoms.<slice>.get().meta.getCellColSpan is deprecated in favor of TanStack's built-in cell spanning. It
still works — it is mapped onto spanColumns for you — but migrate when convenient:
Return 1 rather than undefined for "no span". In exchange you can return Infinity
to span the rest of the row, which the old callback could not express. Read the resolved
values with cell.getColSpan() and skip cells where cell.getIsCovered() is true.
This migration is manual: the callback's argument changed from the cell to
{ row, column, table }, so it is not safe to rewrite mechanically.
Shift-clicking a row — or its selection checkbox, or Shift+Space on a focused checkbox — now selects a range of rows. This is new behavior rather than a breaking change, but it is worth knowing about if you have tests that shift-click.
Related to that, a click on a selection checkbox no longer also runs the row's click handler.
Previously both fired, so a single click toggled the row twice and called
onRowSelectionChange twice.
Early in the Pluma rollout, several components shipped an opt-in "legacy" style override so they could visually match their old Fly counterparts while apps migrated incrementally. These overrides have been removed — components only render in their current (default) Pluma styles.
unsafe_isLegacy prop removed from:
ButtonDatePickerLinkSelectTextAreaTextFieldToggleButtonPlumaProvider componentFlags removed:
unsafe_useLegacyButtonunsafe_useLegacyTextFieldunsafe_useLegacyTextAreaunsafe_useLegacySelectunsafe_useLegacyLinkRemove these props and flags from your app; the affected components already render in the target style by default.
Icon sizeVersion prop is goneVarious individual icons were deprecated and have been either:
error → critical, warning → caution)The icon-deprecations codemod handles the renames and merges, folds v1-only sizes (xxs/xs → sm), and removes the Icon sizeVersion prop.
get functionWhen using PlumaTestHelpers, developers should not use the get function anymore. Instead they should use extract and find.
Run the test-helper-get-rename codemod to rewrite get calls to find. get and find share a signature and container-scoped lookup, so the rename is behavior-preserving in the common case; the codemod flags every rewritten site so you can switch the ones that relied on get's self-matching (the container was the component's own root element) to extract.
For more information, please visit the Test Helpers docs.
To migrate to v3:
Before you start — on the previous major: migrate Button variants. Set unsafe_withMigratedButtonVariants on PlumaProvider and run the button-variant-rename codemod to rename tertiary → secondary and subtle → tertiary in your source, then upgrade and drop the flag. This step is deliberately outside the numbered list: it wants the previous major, where the flag still exists so the rename causes no visual regression.
It also has to happen before step 8 below. toggle-button-to-button converts ToggleButton to Button and adds variant="tertiary" — already the new vocabulary — so running button-variant-rename afterwards would migrate that value a second time and land those buttons on secondary, one tier too heavy. It fails silently, so order is the only defense.
medium, small, large with md, sm, lg (run the size-longhand-rename codemod)s/m/l with sm/md/lg — props via the text-size-shorthand-rename codemod, raw CSS custom properties via the css-token-rename codemod*Primitive names to their new names (PlainPopover, PlainModal, SelectController) — run the primitive-rename codemodonChange to onCheckedChange where applicable (run the event-handler-rename codemod)deprecated-props-removal codemod)unsafe_leftSectionComponent / unsafe_rightSectionComponent with elementBefore / elementAfter on TextField and Selectunsafe_isLegacy props and unsafe_useLegacy* PlumaProvider flags — components already render in the target style by defaultToggleButton usage to Button (run the toggle-button-to-button codemod)LabelIcon usage to the Coin component (run the label-icon-to-coin codemod). Do this before step 12: icon-deprecations only rewrites deprecated icon names on Icon itself, so a deprecated name on a LabelIcon is skipped silently if it runs first, then carried onto the emitted IconLabel usage to Badge with the color prop, and Badge's isOutline/isEmphasized to their color equivalents (run the label-to-badge codemod; review anything it flags)span instead of a div — update HTMLDivElement ref types and any div-based selectors, or pass as="div" where the previous element is requiredicon-deprecations codemod). Run this after step 9get with extract or find (run the test-helper-get-rename codemod)flexRenderComponent and @ctx/@options, add the features type parameter to your column helpers, and rename sortingFn → sortFn and table.getState() → table.store.state (run the data-table-tanstack-v9 codemod; meta.getCellColSpan is deprecated but still works)We recommend migrating incrementally and testing thoroughly. These APIs have been removed in v3, so run the codemods above to migrate your source before upgrading.