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:
primeng16.9.1 and 21.1.10 source, with@primeuix/styles2.0.3.
Table of Contents
- Phase 0: Before you start
- Phase 1: Renames and removed components
- Phase 2: Theming and CSS
- Phase 3: Templates, inputs and signals
- Phase 4: Component behaviour
- Phase 5: Wrappers, performance, tests and Angular 21
- 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/resourcestheme imports fromangular.jsonand global styles. v18+ themes come from@primeuix/themes. - ☐ Pin
@primeuix/themesto the same major as the@primeuix/stylesthat PrimeNG pulls in. PrimeNG 21.1.x depends on@primeuix/styles ^2.0.3, but a barenpm i @primeuix/themestoday installs 3.x. - ☐ Make sure
@primeuix/stylesand@primeuix/themesare 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
$styleperformance 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
strictTemplateson 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, soNG8001won’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 - ☐
TabViewandTabMenu→Tabs(p-tabs,p-tablist,p-tab,p-tabpanels,p-tabpanel) - ☐
TriStateCheckbox→Checkboxwithindeterminate - ☐
Messages→ onep-messageper message - ☐
Chips→AutoCompletewithmultipleand[typeahead]="false" - ☐
Accordion+AccordionTab→p-accordionwithp-accordion-panel,p-accordion-header,p-accordion-content - ☐
VirtualScroller→p-scrolleror a virtual-scrolledp-dataview - ☐
Animate→AnimateOnScroll;Defer→ Angular’s@defer - ☐
SlideMenuandSpinner→ removed; pick a replacement (TieredMenu/PanelMenu,InputNumber)
Event and type renames
- ☐ Event types follow the new names:
DropdownChangeEvent→SelectChangeEvent,InputSwitchChangeEvent→ToggleSwitchChangeEvent, and v17’sAutoCompleteOnSelectEvent→AutoCompleteSelectEvent - ☐
DropdownItemis now the componentSelectIteminprimeng/select. It collides with theSelectIteminterface inprimeng/api, so alias one of them. - ☐ ⚠️ Deep imports like
primeng/api/menuitemandprimeng/api/treenodeno longer resolve. Import fromprimeng/api.
Module re-exports
- ☐ ⚠️ Import
RouterModuleyourself whereverMenu,TieredMenu,BreadcrumborStepssupplied it. In v21 those modules no longer re-export it, androuterLinkgoes silently inert. - ☐ ⚠️ Import
FormsModuleyourself whereverPaginatorModulesupplied it, andTieredMenuModulewhereverSplitButtonModuledid.
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 innode_modules/@primeuix/styles/dist/<component>/index.mjsagainst your preset’s keys. A missing token resets the property to its initial value (no gutter, no arrow, wrong size). - ☐ ⚠️ Set
darkModeSelectorexplicitly. The default is'system', which follows the user’s OS. - ☐ ⚠️ Decide on
cssLayer. With the defaultfalse, 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
inputStyleconfig toinputVariant.
Selectors and variables
- ☐ ⚠️ Make descendant selectors compound where the root class moved onto the host:
.banner .p-message→.banner.p-message. Applies toMessage,AccordionPanel,Tabs,Select,AutoCompleteand others. - ☐ ⚠️ Remove selectors scoped under
.p-element. OnlyTreeTablestill 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
.tsfiles as well as.scss:closest(),querySelector(),classListandMenuItem.styleClassstrings 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
trborders totd:first-child..p-datatable-tableusesborder-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-selectedonly applies whencheckmarkis off, and.p-multiselect-option-selectedonly withhighlightOnSelect. Use[data-p-highlight="true"]to cover both. - ☐
.p-highlightelsewhere: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-datepickeris now always true. - ☐
Scroller:.p-scroller*→.p-virtualscroller* - ☐
InputNumber: buttons arebutton.p-inputnumber-button, notpButton. Severity classes inincrementButtonClassdo 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) - ☐
DialogandToastclose 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/rightis gone. UseIconFieldwithInputIcon.
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.
- ☐
AutoCompletefield→optionLabel - ☐
AutoComplete,Select,MultiSelectitemSize→virtualScrollItemSize - ☐
TableandTreeTablevirtualRowHeight,TreevirtualNodeHeight→virtualScrollItemSize - ☐
MultiSelectdefaultLabel→placeholder - ☐
Select,MultiSelect,TreeSelectbaseZIndex,autoZIndex,showTransitionOptions,hideTransitionOptions→overlayOptions - ☐
DatePickermonthNavigator,yearNavigator,yearRange,locale→ removed; set locale through PrimeNG’stranslationconfig - ☐
SelectautoDisplayFirst→ removed. Add a placeholder or set the initial value yourself. - ☐
DialogpositionLeft,responsive,breakpoint→ removed - ☐
InputMaskautoFocus→autofocus;pAutoFocus’sautofocusproperty →[pAutoFocus]="true" - ☐
PanelexpandIcon,collapseIcon→headericonstemplate - ☐
TableautoLayout,responsive,scrollDirection→ removed - ☐
AccordionheaderAriaLevel→aria-levelon the header - ☐
StyleClassenterClass,leaveClass→enterFromClass,leaveFromClass - ☐
MultiSelectcheckicontemplate →headercheckboxicon,itemcheckboxicon - ☐ ⚠️
CheckboxandRadioButtonlabel,labelStyleClass→ removed. Use a real<label for>. - ☐
pInputText/pTextareasize→pSize.size="small"now sets the nativesizeattribute.
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→classon host-styled components - ☐
pButtonicon,iconPos,label,loadingIcon→pButtonIcon/pButtonLabeldirectives - ☐
ButtonbadgeClass→badgeSeverity. ⚠️ It’s already ignored by the template, and badges default tosecondary(grey). - ☐
Badgesize→badgeSize - ☐
AutoCompleteminLength→minQueryLength;PasswordmaxLength→maxlength;PaginatordropdownAppendTo→appendTo - ☐
Messagetext,escape→ content projection - ☐
pBadgedirective →OverlayBadgecomponent
Template refs and slots
- ☐ ⚠️
TablepTemplate="rowexpansion"→"expandedrow", and"frozenrowexpansion"→"frozenexpandedrow". The old names are ignored. - ☐
DataViewgridItem/listItemtemplates →grid/list, with contextitemsinstead ofdataandindex(v17). - ☐ ⚠️ Named
#refsare lowercase:#selecteditems,#emptyfilter,#loadingicon. A wrong case never renders. - ☐ ⚠️ A named
#itemtemplate shadows a row’slet-itemin the same scope. KeeppTemplate="item"where the names collide. - ☐ ⚠️
p-cellEditorhas no named refs. KeeppTemplate="input"andpTemplate="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 forwardingappendTo. A bare signal makes the overlay render far from its field. - ☐ ⚠️ Update
modelValuewith.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@ViewChildwrites likecmp.disabled = true. - ☐ ⚠️
[attr.required]="required"always sets the attribute. Userequired() ? '' : undefined. - ☐ ⚠️ Fix boolean bindings that pass
''or0. WithbooleanAttribute, both mean true. Bind!!expr. - ☐ ⚠️ Replace
<SpinnerIcon [styleClass]>-style icon usage with<svg data-p-icon="spinner" class="…">. Icons are attribute components with only aspininput.
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 totrue. - ☐ Set
[autoOptionFocus]="true"if you relied on the first option being pre-focused. The default flipped tofalse(⚠️ onAutoCompletetoo, 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). v16Dropdownfired it on init viaautoDisplayFirst;Selectdoesn’t. - ☐ ⚠️ Initialise optional selects with
null, not''. An empty string shows the clear icon. - ☐ ⚠️ Turn on
virtualScrollforMultiSelects 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. Everyp-buttonrenders it unlessautofocusis explicitlyfalse. - ☐ ⚠️ 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
showCloseIconwith your own close button.Popoverhas neither the input nor a close template. - ☐ ⚠️ Add
(mousedown)="$event.stopPropagation()"next to every clickstopPropagation()inside a popover, or it won’t close on the next outside click. - ☐ ⚠️ Add
top: 0; left: 0to the popover’sstyleClassif it’s appended to body. - ☐ ⚠️ Clear
p-popover-flippedanddata-p-popover-flippedbefore re-aligning a popover against a new target. - ☐ ⚠️ Update arrow styles:
--overlayArrowLeft→--p-popover-arrow-left. Useborder-style: solid, not thebordershorthand, or you’ll draw two arrows.
Dialog and DynamicDialog
- ☐ ⚠️ Pass
closable: true, draggable: false, resizable: falsein everyDialogService.open(). All three defaults changed. - ☐ Null-check the ref from
open(). From v20 it returnsnullif a dialog of that component type is already open. Passduplicate: truewhere stacking is intended. - ☐ ⚠️ Defer any dialog self-close that can happen in the same render as the open, or the next
open()returnsnull.
DatePicker (was Calendar)
- ☐ ⚠️ Day-align
minDateandmaxDate. A value whose full timestamp falls outside them renders blank. - ☐ ⚠️ Constrain the host width. The popup’s
min-widthnow 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 inlineDatePickerpanel. Stock PrimeNG uses[appear]="!inline".
Checkbox, RadioButton, FileUpload
- ☐ ⚠️ Add
(click)="$event.preventDefault()"to checkboxes inside clickable rows. The native input’schangenow toggles too. - ☐ ⚠️ Delete leftover
binary="true"on radio buttons. In v21 it’s a real input, and it checks the whole group. - ☐ ⚠️ Expect
FileUploadbasic mode to show the file name in a span next to the button, not on the button. Adjust layout or override#filelabel. - ☐ ⚠️ The
Tableboolean column filter can’t cycle back to “any” withoutTriStateCheckbox. Rely on the Clear button.
Message, Badge, Accordion
- ☐ ⚠️ Pass
iconto everyp-message. Severity no longer picks one. - ☐ Set
lifeon messages that should auto-hide. Disable the default enter/leave animation if layout shift bothers you. - ☐ ⚠️ A
Badgewith an emptyvaluerenders 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-panela stable[value]. Open state matches by value, so index-based “expand all” breaks when a panel is hidden. - ☐ ⚠️ Don’t
display: nonecollapsed 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 tofalse.
Tabs, Menu, Scroller, Table
- ☐ Rebuild
TabMenunavigation by hand.Tabshas nomodel,commandorrouterLink. - ☐ ⚠️ Use
classonp-tabs, notstyleClass. It has nostyleClassinput. - ☐ ⚠️ 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 popupp-menustays in the DOM. - ☐ ⚠️ Don’t call
setInitialState()+init()on a liveScroller. The deferred init leaves a blank-list window. - ☐ ⚠️ Add
overflow-anchor: noneto.p-virtualscrollerif the list snaps back near the bottom. - ☐
contextMenuSelectionMode="joint"is deprecated onTable,TreeandTreeTable.
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 callingsuper(…). - ☐ ⚠️ Override
onInit,onChanges,onAfterViewInit,onDestroyinstead of thengXxxhooks. Or always callsuper.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
@Inputto a subclass, check the base’squeriesmetadata. A same-named@ContentChild(likeloadingIconTemplate) resets your input toundefined. - ☐ ⚠️ Check new base-class members for name clashes with your wrapper (
onClose,autofocus,binary,clickInProgress). - ☐ ⚠️ Bind
(onBeforeEnter)and(onAfterLeave)in forked overlay templates.onAnimationStartandonAnimationDonenever fire. - ☐ ⚠️ Read
xTemplate || _xTemplatefor every template slot in forked templates. - ☐ ⚠️ On
DatePicker, overridewriteControlValue, notwriteValue. - ☐ ⚠️ In forked
Tabletemplates, sub-directives reference the table asdataTable, notdt.dtis now the design-token input. - ☐ ⚠️ Create PrimeNG directives through
hostDirectives, nevernew Tooltip(). Constructing one outside an injection context throwsNG0203. - ☐ ⚠️ Drop
[ngStyle]="$style"from forked templates.$styleis the theme bundle, not the consumer’s inline style. - ☐ ⚠️ Remember overlays (
Popover,Selectpanels) still needstyleClass.classlands on the in-place host, not the body-appended panel.
Performance
- ☐ ⚠️ Profile a heavy table or form.
BaseComponent.$styleis 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 inngAfterViewInittorunOutsideAngular. Heavier host bindings turn them into tick storms. - ☐ Replace hand-rolled ellipsis checks (
clientWidth < scrollWidth) withTooltip’sshowOnEllipsis.
Unit tests
- ☐ ⚠️ Make PrimeNG config mocks expose
pt,unstyled,ptOptionsandoverlayAppendToas signals. - ☐ Remove
provideAnimationsAsync()from tests only if nothing in the app still uses@angular/animationstriggers.
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, addprovideZoneChangeDetection()back explicitly. - ☐
TestBedis zoneless too. Callfixture.changeDetectorRef.markForCheck()beforedetectChanges()inOnPushspecs. - ☐ Two-way bindings to non-assignable expressions (
[(ngModel)]="a && a[k]") are now errors. When splitting them, keep the write in(ngModelChange). - ☐
typeCheckHostBindingsis on:private@HostListenermethods now fail the build. - ☐
CDK21 overlays use the browser top layer and paint above any z-index. SetusePopover: falsein the overlay default config if your toasts must stay on top. - ☐ Replace
HttpClientModulewithprovideHttpClient(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).
- ☐
DialogandDrawerbackdrops disappear after closing. - ☐ Each
DynamicDialogopens, closes and opens again. - ☐ Date fields bounded by
maxDate = todaystill 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.