Migrating to v3

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.

Summary

table
ChangeAffectedCodemod
Long-hand size names removedsize prop on Button, ButtonGroup, ProgressBar, SegmentedControl, Select, TextField, Togglesize-longhand-rename
Short-hand token names removeds/m/l size tokens; size prop on Text, Heading, Paragraph; the font-size-s/m/l and text-product-ps custom propertiestext-size-shorthand-rename, css-token-rename
*Primitive component renamesPopoverPrimitive, ModalPrimitive, SelectPrimitiveprimitive-rename
Component removalsToggleButton, LabelIcon, Labeltoggle-button-to-button, label-icon-to-coin, label-to-badge
Button variant consolidationvariant prop on Button, ButtonGroupbutton-variant-rename
Event handler standardizationonChange → onCheckedChange on OptionCard, Radio, Checkboxevent-handler-rename
Deprecated props removedButton.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 namesdeprecated-props-removal, label-to-badge, css-token-rename
Unsafe section components removedunsafe_leftSectionComponent / unsafe_rightSectionComponent on TextField; unsafe_leftSectionComponent on Select—
DataTable on TanStack Table v9DataTable Ember cell/header components, column definition types, sortingFn, table.getState(), ember-source peer rangedata-table-tanstack-v9
Badge default elementBadge renders a span instead of a div—
Legacy compatibility styles removedunsafe_isLegacy on Button, DatePicker, Link, Select, TextArea, TextField, ToggleButton; unsafe_useLegacy* PlumaProvider flags—
Icon sizing + deprecationsAll icon usageicon-deprecations
Test helper get removedPlumaTestHelperstest-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.

Prop Name Changes

Remove Long Hand Size Names

Long hand size prop values have been removed in favor of shorter, more consistent alternatives:

  • medium → md
  • small → sm
  • large → lg

Affected Components:

  • Button - size prop
  • ButtonGroup - size prop
  • ProgressBar - size prop
  • SegmentedControl - size prop
  • Select - size prop
  • TextField - size prop
  • Toggle - size prop

The size-longhand-rename codemod renames these props across .tsx/.jsx and .gts/.gjs/.hbs files.

Remove Short Hand Token Names

Short hand size token names are expanded to two letters for clarity:

  • s → sm
  • m → md
  • l → lg

This 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.

Component Changes

Rename the *Primitive Components

The *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 → PlainPopover
  • ModalPrimitive → PlainModal
  • SelectPrimitive → SelectController

This 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.

Component Removal

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 Default Element

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:

  • Refs and element types change from HTMLDivElement to HTMLSpanElement (React ref types and the Ember Element signature) — TypeScript will surface every affected site.
  • CSS or test selectors that assume a div need updating.
  • Pass as="div" to retain the previous element where it matters.

Button Variant Consolidation

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:

table
Current variantAfter the major
primaryprimary (unchanged)
secondaryremoved
tertiarysecondary (now the default-looking, less-prominent button)
subtletertiary (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:

  1. On the previous major, set 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.
  2. Upgrade to this major and drop the flag. Your migrated source already uses the new names natively, so rendering doesn't change.

See the codemod README for the full migration order, running instructions, and limitations.

Event Handler Changes

Standardize Event Handler Names

Several components have migrated to more consistent event handler naming:

Change onChange to onCheckedChange:

  • OptionCard.onChange → OptionCard.onCheckedChange
  • Radio.onChange → Radio.onCheckedChange
  • Checkbox.onChange → Checkbox.onCheckedChange

Run the event-handler-rename codemod to rename these handlers across React and Ember.

Deprecated Props Removal

The deprecated-props-removal codemod migrates the Button, ConfirmationModal, DropdownMenu.Item, Link, Snackbar, Popover, and Banner props below; the DataTable sorting changes are manual.

Button Component

  • Remove isError prop (use isCritical instead)
  • Rename isDanger prop to isCritical
  • Rename isError prop to isCritical (same critical color treatment, new name)

ConfirmationModal and DropdownMenu.Item

  • Rename 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).

Snackbar Component

  • Remove isError prop (use variant="critical" instead)
  • Rename error variant to critical
  • Remove warning variant (use caution instead)
  • Rename error variant to critical

Popover Component

  • Remove content prop (use description instead)

Badge Component

  • Remove isEmphasized prop (use color="accent" instead)
  • Remove isOutline prop (use color="neutral-outline" instead, or color="accent-outline" where it was combined with isEmphasized). Run the label-to-badge codemod.

DataTable Component

  • Remove deprecated { id, desc } object form for sorting (use sorting={{ value: { id, desc } }})
  • Remove 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:

sorting={{ value: { id: 'name', desc: false }, onChange: handleSort, isManual: false }}

Leave isManual off (it defaults to true) if you sort the data yourself, e.g. server-side.

Unsafe Section Components

  • Remove TextField.unsafe_leftSectionComponent (use elementBefore instead)
  • Remove TextField.unsafe_rightSectionComponent (use elementAfter instead)
  • Remove 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 and TanStack Table v9

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.

Ember Cell and Header Components

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.

// before
import { flexRenderWrapper } from '@customerio/pluma-components/ember/tanstack/table';
cell: (info) => flexRenderWrapper(StatusCell, { status: info.getValue() });
// StatusCell read @props.status

// after
import { flexRenderComponent } from '@tanstack/ember-table';
cell: (info) => flexRenderComponent(StatusCell, { status: info.getValue() });
// StatusCell reads @options.status, and can now also read @ctx

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.

Ember Version

The ember-source peer range is now >=5.8.0, which @tanstack/ember-table requires.

Column Definition Types

TanStack's types gained a leading features type parameter. DataTableFeatures is exported from both entry points for this:

import type { DataTableFeatures } from '@customerio/pluma-components/react';

const columnHelper = createColumnHelper<DataTableFeatures, Person>();

The same applies to Table, Row, Cell, Column, and ColumnDef.

Renamed Table APIs

  • 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().

Cell Spanning

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:

// before
meta: { getCellColSpan: (cell) => (cell.row.original.isSummary ? 2 : undefined) }

// after
spanColumns: ({ row }) => (row.original.isSummary ? 2 : 1)

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.

Row Range Selection

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.

Legacy Compatibility Styles

Remove Legacy Compatibility Styles

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:

  • Button
  • DatePicker
  • Link
  • Select
  • TextArea
  • TextField
  • ToggleButton

PlumaProvider componentFlags removed:

  • unsafe_useLegacyButton
  • unsafe_useLegacyTextField
  • unsafe_useLegacyTextArea
  • unsafe_useLegacySelect
  • unsafe_useLegacyLink

Remove these props and flags from your app; the affected components already render in the target style by default.

Icon Changes

Icon Sizing

  • v1 icon sizing has been removed — icons now use v2 sizing, and the Icon sizeVersion prop is gone
  • Deprecated icons have been removed without replacement
  • Some icons have been renamed or merged with others

Specific Icon Changes

Various individual icons were deprecated and have been either:

  • Removed without replacement
  • Renamed to new names (including the semantic status icons: error → critical, warning → caution)
  • Merged with existing icons

The icon-deprecations codemod handles the renames and merges, folds v1-only sizes (xxs/xs → sm), and removes the Icon sizeVersion prop.

Test Helper Changes

Remove get function

When 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.

  1. Update size props: Replace medium, small, large with md, sm, lg (run the size-longhand-rename codemod)
  2. Update token names: Replace short-hand sizes 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
  3. Rename primitive components: Update imports and usage from the *Primitive names to their new names (PlainPopover, PlainModal, SelectController) — run the primitive-rename codemod
  4. Update event handlers: Change onChange to onCheckedChange where applicable (run the event-handler-rename codemod)
  5. Remove deprecated props: Replace with recommended alternatives (run the deprecated-props-removal codemod)
  6. Rename the unsafe section props: Replace unsafe_leftSectionComponent / unsafe_rightSectionComponent with elementBefore / elementAfter on TextField and Select
  7. Remove legacy compatibility styles: Drop unsafe_isLegacy props and unsafe_useLegacy* PlumaProvider flags — components already render in the target style by default
  8. Replace ToggleButton with Button: Migrate ToggleButton usage to Button (run the toggle-button-to-button codemod)
  9. Replace LabelIcon with Coin: Migrate 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 Icon
  10. Replace Label with Badge: Migrate Label 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)
  11. Update Badge element assumptions: Badge renders a span instead of a div — update HTMLDivElement ref types and any div-based selectors, or pass as="div" where the previous element is required
  12. Update icons: Migrate to v2 sizing and check for deprecated icon usage (run the icon-deprecations codemod). Run this after step 9
  13. Switch Test Helpers functions: Replace get with extract or find (run the test-helper-get-rename codemod)
  14. Migrate DataTable to TanStack Table v9: Update Ember cell/header components to 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.