By Prateek MishraSoftware Engineering

The Complete PrimeNG v16 → v21 Migration Checklist

150+ checks across six phases, from package versions to the final smoke test. Bookmark it, then run it against your codebase.

A PrimeNG upgrade fails in two places: the changes you didn’t know about, and the ones you knew about but forgot to grep for.

I led a production Angular app through PrimeNG 16 → 21. Afterwards, I turned everything we fixed into one list: the documented changes from the official migration pages, plus the undocumented ones I found only in the source. This is that list.

How to use it

  • Work top to bottom. Phases run in the order you’d hit them: dependencies, renames, theming, templates, behaviour, then the long tail.
  • Each line is one check. It names what to look for and, where it helps, the grep that finds it. Paste the list into your tracker and tick as you go.
  • Items marked ⚠️ are undocumented. They aren’t in any migration guide. The full story behind each one, with the source-level cause, is in I Debugged Every PrimeNG v21 Surprise So You Don’t Have To.
  • Checked against: primeng 16.9.1 and 21.1.10 source, with @primeuix/styles 2.0.3.

Table of Contents

  1. Phase 0: Before you start
  2. Phase 1: Renames and removed components
  3. Phase 2: Theming and CSS
  4. Phase 3: Templates, inputs and signals
  5. Phase 4: Component behaviour
  6. Phase 5: Wrappers, performance, tests and Angular 21
  7. Final smoke test

Phase 0: Before you start

Dependencies

  • ☐ Upgrade one PrimeNG major at a time, in step with Angular. Each PrimeNG major tracks the Angular major of the same number.
  • ☐ Remove primeng/resources theme imports from angular.json and global styles. v18+ themes come from @primeuix/themes.
  • ☐ Pin @primeuix/themes to the same major as the @primeuix/styles that PrimeNG pulls in. PrimeNG 21.1.x depends on @primeuix/styles ^2.0.3, but a bare npm i @primeuix/themes today installs 3.x.
  • ☐ Make sure @primeuix/styles and @primeuix/themes are at least 2.0.2 on v21. Older versions cause visual and animation bugs.
  • ☐ Check whether PrimeNG 22 is a better landing point. It’s out, and it fixes the $style performance issue in Phase 5.

Tooling

  • ☐ ⚠️ Don’t treat a green build as proof. With strictTemplates: false, renamed classes, bare signal reads and removed static-attribute inputs all compile.
  • ☐ ⚠️ Turn on strictTemplates on a throwaway branch once, and list what it finds.
  • ☐ ⚠️ Grep for old camel-case selectors (p-autoComplete, p-multiSelect, p-fileUpload, p-datePicker). They still compile as aliases, so NG8001 won’t find them.
  • ☐ Take screenshots of key screens on v16 now. You’ll want a visual baseline for every phase after this one.

Phase 1: Renames and removed components

These modules exist in 16.9.1 and are gone from 21.1.10. Replace every import.

  • ☐ Dropdown → Select (primeng/select)
  • ☐ Calendar → DatePicker (primeng/datepicker)
  • ☐ InputSwitch → ToggleSwitch
  • ☐ InputTextarea → Textarea (pTextarea)
  • ☐ OverlayPanel → Popover
  • ☐ Sidebar → Drawer
  • ☐ TabView and TabMenu → Tabs (p-tabs, p-tablist, p-tab, p-tabpanels, p-tabpanel)
  • ☐ TriStateCheckbox → Checkbox with indeterminate
  • ☐ Messages → one p-message per message
  • ☐ Chips → AutoComplete with multiple and [typeahead]="false"
  • ☐ Accordion + AccordionTab → p-accordion with p-accordion-panel, p-accordion-header, p-accordion-content
  • ☐ VirtualScroller → p-scroller or a virtual-scrolled p-dataview
  • ☐ Animate → AnimateOnScroll; Defer → Angular’s @defer
  • ☐ SlideMenu and Spinner → removed; pick a replacement (TieredMenu/PanelMenu, InputNumber)

Event and type renames

  • ☐ Event types follow the new names: DropdownChangeEvent → SelectChangeEvent, InputSwitchChangeEvent → ToggleSwitchChangeEvent, and v17’s AutoCompleteOnSelectEvent → AutoCompleteSelectEvent
  • ☐ DropdownItem is now the component SelectItem in primeng/select. It collides with the SelectItem interface in primeng/api, so alias one of them.
  • ☐ ⚠️ Deep imports like primeng/api/menuitem and primeng/api/treenode no longer resolve. Import from primeng/api.

Module re-exports

  • ☐ ⚠️ Import RouterModule yourself wherever Menu, TieredMenu, Breadcrumb or Steps supplied it. In v21 those modules no longer re-export it, and routerLink goes silently inert.
  • ☐ ⚠️ Import FormsModule yourself wherever PaginatorModule supplied it, and TieredMenuModule wherever SplitButtonModule did.

Phase 2: Theming and CSS

Theme setup

  • ☐ Register the theme through providePrimeNG({ theme: { preset, options } }).
  • ☐ Build custom presets with definePreset(Aura, { … }) rather than from scratch, so every component token has a value.
  • ☐ ⚠️ If your preset is standalone, diff every dt('…') name in node_modules/@primeuix/styles/dist/<component>/index.mjs against your preset’s keys. A missing token resets the property to its initial value (no gutter, no arrow, wrong size).
  • ☐ ⚠️ Set darkModeSelector explicitly. The default is 'system', which follows the user’s OS.
  • ☐ ⚠️ Decide on cssLayer. With the default false, PrimeNG’s CSS is unlayered and appended to <head>, so it wins equal-specificity ties and beats anything you put in @layer.
  • ☐ Move any global inputStyle config to inputVariant.

Selectors and variables

  • ☐ ⚠️ Make descendant selectors compound where the root class moved onto the host: .banner .p-message → .banner.p-message. Applies to Message, AccordionPanel, Tabs, Select, AutoComplete and others.
  • ☐ ⚠️ Remove selectors scoped under .p-element. Only TreeTable still emits it.
  • ☐ ⚠️ Replace v16 palette variables (--blue-500, --surface-200 …) and --primary-color-text (now --p-primary-inverse-color). Give every compat alias a literal fallback.
  • ☐ ⚠️ Grep .ts files as well as .scss: closest(), querySelector(), classList and MenuItem.styleClass strings rot silently. Prefer [data-pc-name] and [data-pc-section] for runtime checks.
  • ☐ ⚠️ Override .p-overlay .p-overlay-content { will-change: auto; } if dropdown text looks blurry.
  • ☐ ⚠️ Move table row accents from tr borders to td:first-child. .p-datatable-table uses border-collapse: separate, so row borders don’t paint.

Class renames a prefix swap gets wrong

A blind p-dropdown-* → p-select-* swap produces class names that exist in neither version. Check each against the v21 source.

  • ☐ Select: -item → -option, -items → -list, -items-wrapper → -list-container, -panel → -overlay, -trigger → -dropdown
  • ☐ MultiSelect: -panel → -overlay, -items → -list
  • ☐ AutoComplete: -panel → -overlay, -items → -list, -item → -option, -item-group → -option-group
  • ☐ Selected option: .p-select-option-selected only applies when checkmark is off, and .p-multiselect-option-selected only with highlightOnSelect. Use [data-p-highlight="true"] to cover both.
  • ☐ .p-highlight elsewhere: p-tieredmenu-item-active, p-datepicker-day-selected, p-tab-active
  • ☐ TieredMenu: li > a.p-menuitem-link → li.p-tieredmenu-item > div.p-tieredmenu-item-content > a.p-tieredmenu-item-link (one extra level)
  • ☐ Menu: .p-menuitem-link → .p-menu-item-link
  • ☐ Message: v16’s .p-message-wrapper → .p-message-content-wrapper, which wraps .p-message-content, which wraps .p-message-text
  • ☐ Textarea: .p-inputtextarea → .p-textarea. It no longer carries .p-inputtext, so shared input styles miss it.
  • ☐ ToggleSwitch: .p-inputswitch-slider:before → .p-toggleswitch-handle (a real element now)
  • ☐ DatePicker: the host is now .p-datepicker, and the popup is .p-datepicker-panel. Any “is the panel open?” check on .p-datepicker is now always true.
  • ☐ Scroller: .p-scroller* → .p-virtualscroller*
  • ☐ InputNumber: buttons are button.p-inputnumber-button, not pButton. Severity classes in incrementButtonClass do nothing.
  • ☐ SplitButton: -defaultbutton → -button, -menubutton → -dropdown
  • ☐ Breadcrumb: p-menuitem-link → p-breadcrumb-item-link, p-menuitem-separator → p-breadcrumb-separator
  • ☐ ConfirmDialog: p-confirm-dialog-accept → p-confirmdialog-accept-button (same for reject)
  • ☐ Dialog and Toast close buttons: p-dialog-close-button, p-toast-close-button. The mask is .p-dialog-mask.p-overlay-mask.
  • ☐ Listbox: -item → -option. ProgressSpinner: .p-progress-spinner-circle → .p-progressspinner-circle
  • ☐ .p-input-icon-left/right is gone. Use IconField with InputIcon.

Phase 3: Templates, inputs and signals

Removed inputs

A removed input written as a static attribute (defaultLabel="Select") is just an HTML attribute after the upgrade. Only bound [x] inputs fail the build. Grep for each.

  • ☐ AutoComplete field → optionLabel
  • ☐ AutoComplete, Select, MultiSelect itemSize → virtualScrollItemSize
  • ☐ Table and TreeTable virtualRowHeight, Tree virtualNodeHeight → virtualScrollItemSize
  • ☐ MultiSelect defaultLabel → placeholder
  • ☐ Select, MultiSelect, TreeSelect baseZIndex, autoZIndex, showTransitionOptions, hideTransitionOptions → overlayOptions
  • ☐ DatePicker monthNavigator, yearNavigator, yearRange, locale → removed; set locale through PrimeNG’s translation config
  • ☐ Select autoDisplayFirst → removed. Add a placeholder or set the initial value yourself.
  • ☐ Dialog positionLeft, responsive, breakpoint → removed
  • ☐ InputMask autoFocus → autofocus; pAutoFocus’s autofocus property → [pAutoFocus]="true"
  • ☐ Panel expandIcon, collapseIcon → headericons template
  • ☐ Table autoLayout, responsive, scrollDirection → removed
  • ☐ Accordion headerAriaLevel → aria-level on the header
  • ☐ StyleClass enterClass, leaveClass → enterFromClass, leaveFromClass
  • ☐ MultiSelect checkicon template → headercheckboxicon, itemcheckboxicon
  • ☐ ⚠️ Checkbox and RadioButton label, labelStyleClass → removed. Use a real <label for>.
  • ☐ pInputText / pTextarea size → pSize. size="small" now sets the native size attribute.

Deprecated now, removal planned

These still work on 21 but are on the way out. Cheaper to move now than during the next major.

  • ☐ pTemplate="x" → <ng-template #x>
  • ☐ styleClass → class on host-styled components
  • ☐ pButton icon, iconPos, label, loadingIcon → pButtonIcon / pButtonLabel directives
  • ☐ Button badgeClass → badgeSeverity. ⚠️ It’s already ignored by the template, and badges default to secondary (grey).
  • ☐ Badge size → badgeSize
  • ☐ AutoComplete minLength → minQueryLength; Password maxLength → maxlength; Paginator dropdownAppendTo → appendTo
  • ☐ Message text, escape → content projection
  • ☐ pBadge directive → OverlayBadge component

Template refs and slots

  • ☐ ⚠️ Table pTemplate="rowexpansion" → "expandedrow", and "frozenrowexpansion" → "frozenexpandedrow". The old names are ignored.
  • ☐ DataView gridItem / listItem templates → grid / list, with context items instead of data and index (v17).
  • ☐ ⚠️ Named #refs are lowercase: #selecteditems, #emptyfilter, #loadingicon. A wrong case never renders.
  • ☐ ⚠️ A named #item template shadows a row’s let-item in the same scope. Keep pTemplate="item" where the names collide.
  • ☐ ⚠️ p-cellEditor has no named refs. Keep pTemplate="input" and pTemplate="output" there.

Signals and booleans

  • ☐ ⚠️ Read base inputs as signals in subclasses and forked templates: disabled(), required(), name(), invalid(), size(), fluid(), variant().
  • ☐ ⚠️ Read $appendTo() when forwarding appendTo. A bare signal makes the overlay render far from its field.
  • ☐ ⚠️ Update modelValue with .set(). Plain assignment breaks $filled.
  • ☐ ⚠️ Never assign to a signal input, including the v18-era @Input('size') set customSize(v) { this.size = v } pattern and @ViewChild writes like cmp.disabled = true.
  • ☐ ⚠️ [attr.required]="required" always sets the attribute. Use required() ? '' : undefined.
  • ☐ ⚠️ Fix boolean bindings that pass '' or 0. With booleanAttribute, both mean true. Bind !!expr.
  • ☐ ⚠️ Replace <SpinnerIcon [styleClass]>-style icon usage with <svg data-p-icon="spinner" class="…">. Icons are attribute components with only a spin input.

Phase 4: Component behaviour

These compile and render. They just behave differently.

Select, MultiSelect, AutoComplete

  • ☐ ⚠️ Set [focusOnHover]="false" where you don’t want the hover highlight to stick. The default flipped to true.
  • ☐ Set [autoOptionFocus]="true" if you relied on the first option being pre-focused. The default flipped to false (⚠️ on AutoComplete too, which the CHANGELOG doesn’t mention).
  • ☐ ⚠️ Set MultiSelect [autofocusFilter]="true" if the filter should take focus on open.
  • ☐ ⚠️ Move load-time logic out of (ngModelChange). v16 Dropdown fired it on init via autoDisplayFirst; Select doesn’t.
  • ☐ ⚠️ Initialise optional selects with null, not ''. An empty string shows the clear icon.
  • ☐ ⚠️ Turn on virtualScroll for MultiSelects with hundreds of options. Each option is now a full component with a nested checkbox.

Buttons and focus

  • ☐ ⚠️ Audit button[autofocus] on a few pages. Every p-button renders it unless autofocus is explicitly false.
  • ☐ ⚠️ Set [focusOnShow]="false" on popovers and dialogs whose trigger should keep focus.
  • ☐ ⚠️ Expect Dialog’s initial focus to go to content first, then footer, then header. In v16 it usually landed on the header close button.

Popover (was OverlayPanel)

  • ☐ Replace showCloseIcon with your own close button. Popover has neither the input nor a close template.
  • ☐ ⚠️ Add (mousedown)="$event.stopPropagation()" next to every click stopPropagation() inside a popover, or it won’t close on the next outside click.
  • ☐ ⚠️ Add top: 0; left: 0 to the popover’s styleClass if it’s appended to body.
  • ☐ ⚠️ Clear p-popover-flipped and data-p-popover-flipped before re-aligning a popover against a new target.
  • ☐ ⚠️ Update arrow styles: --overlayArrowLeft → --p-popover-arrow-left. Use border-style: solid, not the border shorthand, or you’ll draw two arrows.

Dialog and DynamicDialog

  • ☐ ⚠️ Pass closable: true, draggable: false, resizable: false in every DialogService.open(). All three defaults changed.
  • ☐ Null-check the ref from open(). From v20 it returns null if a dialog of that component type is already open. Pass duplicate: true where stacking is intended.
  • ☐ ⚠️ Defer any dialog self-close that can happen in the same render as the open, or the next open() returns null.

DatePicker (was Calendar)

  • ☐ ⚠️ Day-align minDate and maxDate. A value whose full timestamp falls outside them renders blank.
  • ☐ ⚠️ Constrain the host width. The popup’s min-width now equals the host’s width.
  • ☐ ⚠️ If you copied the v16 template, [ngClass]="[todayButtonStyleClass]" now throws, because that input lost its 'p-button-text' default.
  • ☐ Don’t set [pMotionAppear]="true" on an inline DatePicker panel. Stock PrimeNG uses [appear]="!inline".

Checkbox, RadioButton, FileUpload

  • ☐ ⚠️ Add (click)="$event.preventDefault()" to checkboxes inside clickable rows. The native input’s change now toggles too.
  • ☐ ⚠️ Delete leftover binary="true" on radio buttons. In v21 it’s a real input, and it checks the whole group.
  • ☐ ⚠️ Expect FileUpload basic mode to show the file name in a span next to the button, not on the button. Adjust layout or override #filelabel.
  • ☐ ⚠️ The Table boolean column filter can’t cycle back to “any” without TriStateCheckbox. Rely on the Clear button.

Message, Badge, Accordion

  • ☐ ⚠️ Pass icon to every p-message. Severity no longer picks one.
  • ☐ Set life on messages that should auto-hide. Disable the default enter/leave animation if layout shift bothers you.
  • ☐ ⚠️ A Badge with an empty value renders as a dot. Hide it instead.
  • ☐ ⚠️ Rotate accordion icons on .p-accordionheader[data-p-active]. The icons changed from right/down to down/up.
  • ☐ ⚠️ Give each p-accordion-panel a stable [value]. Open state matches by value, so index-based “expand all” breaks when a panel is hidden.
  • ☐ ⚠️ Don’t display: none collapsed accordion content. It stays mounted, and hiding it kills the open animation.

Tree, PickList, OrderList

  • ☐ ⚠️ Set [metaKeySelection]="true" if users should need Ctrl/Cmd for multiple selection. The default flipped to false.

Tabs, Menu, Scroller, Table

  • ☐ Rebuild TabMenu navigation by hand. Tabs has no model, command or routerLink.
  • ☐ ⚠️ Use class on p-tabs, not styleClass. It has no styleClass input.
  • ☐ ⚠️ Forward keyboard events from a capture-phase listener if you have window-level shortcuts. A focused tab stops propagation of every keydown.
  • ☐ ⚠️ Check “is an overlay open?” logic with getClientRects().length, not element presence. A closed popup p-menu stays in the DOM.
  • ☐ ⚠️ Don’t call setInitialState() + init() on a live Scroller. The deferred init leaves a blank-list window.
  • ☐ ⚠️ Add overflow-anchor: none to .p-virtualscroller if the list snaps back near the bottom.
  • ☐ contextMenuSelectionMode="joint" is deprecated on Table, Tree and TreeTable.

Phase 5: Wrappers, performance, tests and Angular 21

If you extend PrimeNG components

  • ☐ Remove redundant constructors from subclasses; most bases now use inject(). Those that still take arguments (InputNumber, DatePicker, AutoComplete, Scroller, Tooltip) each have their own signature, so check before calling super(…).
  • ☐ ⚠️ Override onInit, onChanges, onAfterViewInit, onDestroy instead of the ngXxx hooks. Or always call super.ngXxx(). Skipping it skips style loading.
  • ☐ ⚠️ Audit inherited host bindings on wrapper hosts: root classes, p-disabled, p-focus, data-p-*. Scope theme rules with :not(your-wrapper).
  • ☐ ⚠️ Remove (click) handlers in forked templates that duplicate an inherited host listener (Select, MultiSelect, ToggleSwitch). They fire twice.
  • ☐ ⚠️ Before adding an @Input to a subclass, check the base’s queries metadata. A same-named @ContentChild (like loadingIconTemplate) resets your input to undefined.
  • ☐ ⚠️ Check new base-class members for name clashes with your wrapper (onClose, autofocus, binary, clickInProgress).
  • ☐ ⚠️ Bind (onBeforeEnter) and (onAfterLeave) in forked overlay templates. onAnimationStart and onAnimationDone never fire.
  • ☐ ⚠️ Read xTemplate || _xTemplate for every template slot in forked templates.
  • ☐ ⚠️ On DatePicker, override writeControlValue, not writeValue.
  • ☐ ⚠️ In forked Table templates, sub-directives reference the table as dataTable, not dt. dt is now the design-token input.
  • ☐ ⚠️ Create PrimeNG directives through hostDirectives, never new Tooltip(). Constructing one outside an injection context throws NG0203.
  • ☐ ⚠️ Drop [ngStyle]="$style" from forked templates. $style is the theme bundle, not the consumer’s inline style.
  • ☐ ⚠️ Remember overlays (Popover, Select panels) still need styleClass. class lands on the in-place host, not the body-appended panel.

Performance

  • ☐ ⚠️ Profile a heavy table or form. BaseComponent.$style is uncached on 21 and rebuilt up the parent chain on every read. PrimeNG 22 caches it.
  • ☐ ⚠️ Move scroll and wheel listeners and in-zone setTimeouts in ngAfterViewInit to runOutsideAngular. Heavier host bindings turn them into tick storms.
  • ☐ Replace hand-rolled ellipsis checks (clientWidth < scrollWidth) with Tooltip’s showOnEllipsis.

Unit tests

  • ☐ ⚠️ Make PrimeNG config mocks expose pt, unstyled, ptOptions and overlayAppendTo as signals.
  • ☐ Remove provideAnimationsAsync() from tests only if nothing in the app still uses @angular/animations triggers.

The Angular 21 side

Not PrimeNG, but you’ll hit them in the same upgrade.

  • ☐ Angular 21 bootstraps zoneless by default. If you still need zone.js, add provideZoneChangeDetection() back explicitly.
  • ☐ TestBed is zoneless too. Call fixture.changeDetectorRef.markForCheck() before detectChanges() in OnPush specs.
  • ☐ Two-way bindings to non-assignable expressions ([(ngModel)]="a && a[k]") are now errors. When splitting them, keep the write in (ngModelChange).
  • ☐ typeCheckHostBindings is on: private @HostListener methods now fail the build.
  • ☐ CDK 21 overlays use the browser top layer and paint above any z-index. Set usePopover: false in the overlay default config if your toasts must stay on top.
  • ☐ Replace HttpClientModule with provideHttpClient(withInterceptorsFromDi()), or your interceptors may be skipped.
  • ☐ The application builder outputs to dist/<app>/browser/. Update deploy scripts.
  • ☐ Standalone components: an unimported attribute-only directive like <td pFrozenColumn> is just an attribute. No error.

Final smoke test

Run this by hand on the screens your users touch most. Each line caught at least one real bug for us.

  • ☐ Every dropdown, popover and menu closes on the first outside click, including after typing in a filter and after clicking inside the panel.
  • ☐ Opening a popover or dialog doesn’t move focus off its trigger (unless you want it to).
  • ☐ Dialog and Drawer backdrops disappear after closing.
  • ☐ Each DynamicDialog opens, closes and opens again.
  • ☐ Date fields bounded by maxDate = today still show today’s value.
  • ☐ Hovering dropdown options never leaves two highlighted rows.
  • ☐ Popovers near the right edge of the screen don’t trigger a scrollbar or land short.
  • ☐ Virtual-scrolled lists render rows the moment they open, and don’t go blank on fast scroll.
  • ☐ Checkboxes inside clickable rows toggle exactly once.
  • ☐ With your OS in dark mode, the app still renders in your intended theme.
  • ☐ Keyboard shortcuts still work after clicking a tab.

That’s the list

If you ran all of it, your migration is in better shape than ours was the first time through.

The ⚠️ items are the ones worth understanding, not just ticking. Each has its full story (what changed in the source, why it breaks, how we fixed it) in the companion piece: I Debugged Every PrimeNG v21 Surprise So You Don’t Have To.

For the step-by-step migration path, see Migrating PrimeNG from v16 to v21: A Version-by-Version Guide.