abp-modal z-index and contentChild Issues After ABP 10.6 + Angular 22 UpgradeDate: 2026-10-27
Product: ABP Commercial (Angular UI)
ABP Version: 10.6.0
Angular Version: 22.1.4
Affected Package: @abp/ng.theme.shared v10.6.0
Priority: High
Related JIRA Tickets: VL-246XX (Issue 4), VL-246XX
After upgrading from ABP 8.3 + Angular 17 to ABP 10.6 + Angular 22, <abp-modal> exhibits two critical issues when used inside components that are themselves opened as Angular Material (MatDialog) or CDK overlay-based dialogs:
abp-modal (ng-bootstrap modal) renders behind the CDK overlay container, making it invisible/inaccessible to the user.contentChild('abpBody') signal query fails: When <ng-template #abpBody> is conditionally wrapped with *ngIf, ABP 10.6's new signal-based contentChild() query cannot find the template reference, resulting in an empty modal body.These issues did not exist in ABP 8.3 + Angular 17. They are regressions introduced by the ABP 10.6 migration to Angular Signals-based content queries and the interaction between ng-bootstrap's modal z-index and CDK overlay z-index.
TASConnect Litmus is a multi-tenant Supply Chain Finance (SCF) and Trade Finance SaaS platform. The upgrade was a full-stack migration:
| Component | Before | After | |-----------|--------|-------| | Angular | 17.3.12 | 22.1.4 | | ABP Framework (Angular) | 8.3.0 | 10.6.0 | | ABP Framework (.NET) | 8.3.1 | 10.6.0 | | TypeScript | 5.3.0 | 6.0.0 | | .NET | 8.0 | 10.0 | | Angular Material | 17.3.10 | 22.1.4 | | Angular CDK | 17.3.10 | 22.1.4 | | ng-bootstrap | 16.0.0 | 21.0.0 | | ng-zorro-antd | 17.4.1 | 22.0.1 |
740 files modified, ~738 components updated, 50+ DTOs fixed, 17 modules refactored, 272 .csproj files updated on backend.
abp-modal Appears Behind CDK Overlay (Z-index Conflict)When <abp-modal> is used inside a component that is itself opened as a MatDialog (Angular Material dialog), the abp-modal renders behind the MatDialog overlay and is completely inaccessible to the user. The user sees the parent dialog but cannot see or interact with the abp-modal that should appear on top.
MatDialog (CDK overlay-based dialog)<abp-modal> to open (e.g., click "View Documents")abp-modal appears on top of the MatDialogabp-modal appears behind the MatDialog and is inaccessibleThe issue is a z-index conflict between three CSS systems:
The application's global stylesheet (dark.css) sets the CDK overlay container z-index:
/* dark.css, line 1163 */
.cdk-overlay-container {
z-index: 10000 !important;
left: 0px !important;
position: fixed !important;
}
The application's global stylesheet also sets ng-bootstrap modal z-index:
/* dark.css, line 3340-3348 */
ngb-modal-backdrop.modal-backdrop {
z-index: 9999 !important;
transition: none !important;
}
ngb-modal-window.modal {
z-index: 9999 !important;
transition: none !important;
}
Additionally, NgbModalBackdrop has a hardcoded inline style in its @Component host metadata:
// @ng-bootstrap/ng-bootstrap - NgbModalBackdrop component
@Component({
selector: 'ngb-modal-backdrop',
host: {
'[class]': '"modal-backdrop" + (backdropClass ? " " + backdropClass : "")',
style: 'z-index: 1055', // ← Hardcoded inline style
},
})
| Layer | Element | z-index | Source |
|-------|---------|---------|--------|
| CDK overlay container | .cdk-overlay-container | 10000 !important | dark.css line 1163 |
| ng-bootstrap modal window | ngb-modal-window.modal | 9999 !important | dark.css line 3345 |
| ng-bootstrap modal backdrop | ngb-modal-backdrop.modal-backdrop | 9999 !important | dark.css line 3340 |
| ng-bootstrap backdrop inline | ngb-modal-backdrop | 1055 | Hardcoded host style |
Since 9999 < 10000, the ng-bootstrap modal (used by abp-modal) always renders behind the CDK overlay container (used by MatDialog). This means any abp-modal opened from within a MatDialog is invisible.
In ABP 8.3 + Angular 17, this was not an issue because:
From @abp/ng.theme.shared v10.6.0 (fesm2022 bundle), the ModalComponent.toggle() method:
// @abp/ng.theme.shared - ModalComponent.toggle() (line ~1826) — simplified
this.modalRef = this.modal.open(this.modalContent(), {
size: 'md', centered: false, keyboard: false, scrollable: true,
...this.options(), // ← Spreads user options to ng-bootstrap
windowClass: `${this.options().windowClass || ''} ${this.modalIdentifier}`,
});
this.options() is spread into ng-bootstrap's modal.open(). This means windowClass and backdropClass from [options] input pass through to ng-bootstrap. This is the only mechanism to influence the modal's z-index from the consumer side.
abp-modal does not expose any z-index configuration — consumers cannot set z-index through the component API.document.body — outside the component's view encapsulation, making scoped CSS impossible without ::ng-deep.style: 'z-index: 1055' — this cannot be overridden by CSS classes without !important.We pass custom CSS classes via [options] and use ::ng-deep with !important to override z-index:
<!-- alert.component.html -->
<abp-modal
[options]="{
size: 'md',
windowClass: 'above-cdk-overlay',
backdropClass: 'above-cdk-overlay-backdrop'
}"
[(visible)]="openViewDocument"
>
<!-- modal content -->
</abp-modal>
/* alert.component.scss */
::ng-deep ngb-modal-window.above-cdk-overlay {
z-index: 10001 !important;
}
::ng-deep ngb-modal-backdrop.above-cdk-overlay-backdrop {
z-index: 10001 !important;
}
Problems with this workaround:
::ng-deep (deprecated in Angular, may be removed in future versions)!important to override both the stylesheet rule and inline style<abp-modal> that could appear above a CDK overlay[options] passes through to ng-bootstrapzIndex input to ModalComponent that sets z-index on both the modal window and backdrop.container option that allows specifying a container element (ng-bootstrap supports this). If the modal could be appended inside the CDK overlay container, it would inherit the CDK z-index context.windowClass/backdropClass passthrough — this is currently undocumented but is the only workaround available.@angular/cdk. This would eliminate the z-index conflict entirely.contentChild('abpBody') Signal Query Fails with *ngIf on <ng-template>ABP 10.6's ModalComponent uses Angular Signals-based contentChild() queries:
// @abp/ng.theme.shared v10.6.0 - ModalComponent
this.abpBody = contentChild('abpBody', /* ... */);
In ABP 8.3, this was a @ContentChild decorator-based query. The migration to contentChild() signal introduces a breaking behavioral change when *ngIf is used on <ng-template>.
This pattern worked in ABP 8.3 + Angular 17 but breaks in ABP 10.6 + Angular 22:
<!-- BROKEN in ABP 10.6 + Angular 22 -->
<abp-modal [(visible)]="openViewDocument">
<ng-template *ngIf="!isFxPayModule" #abpBody>
<app-component-a [IsViewOnly]="true"></app-component-a>
</ng-template>
<ng-template *ngIf="isFxPayModule" #abpBody>
<app-component-b [IsViewOnly]="false"></app-component-b>
</ng-template>
</abp-modal>
In Angular 22, *ngIf is syntactic sugar for an implicit <ng-template> wrapper. When *ngIf is placed on a <ng-template>, Angular creates a nested template structure:
<!-- What Angular 22 actually creates: -->
<ng-template [ngIf]="!isFxPayModule">
<ng-template #abpBody>
<app-component-a></app-component-a>
</ng-template>
</ng-template>
The #abpBody template reference is now inside an outer *ngIf template. Angular's contentChild() signal query searches the direct content children of the component. Since #abpBody is wrapped inside the *ngIf template, it's no longer a direct content child — contentChild('abpBody') returns undefined.
ABP's ModalComponent template uses:
<!-- ABP ModalComponent template -->
@if (abpBody()) {
<div id="abp-modal-body" class="modal-body">
<ng-container *ngTemplateOutlet="abpBody()"></ng-container>
</div>
}
Since abpBody() is undefined, the @if block evaluates to false, and the modal body is never rendered — the modal opens but appears empty.
Consolidate into a single <ng-template #abpBody> and move the *ngIf to the child components:
<!-- WORKING in ABP 10.6 + Angular 22 -->
<abp-modal [(visible)]="openViewDocument">
<ng-template #abpBody>
<app-component-a *ngIf="!isFxPayModule" [IsViewOnly]="true"></app-component-a>
<app-component-b *ngIf="isFxPayModule" [IsViewOnly]="false"></app-component-b>
</ng-template>
</abp-modal>
@ContentChild decorator which could find #abpBody even when wrapped in structural directives in some cases, or the pattern was resilient to it.contentChild() signal which has stricter matching behavior for content projection.*ngIf on <ng-template #abpBody> will break.*ngIf to switch between view/edit modes, different user roles, etc.contentChildren() (plural) with a filter, or @ContentChild with { descendants: true } equivalent, to find #abpBody even when wrapped in structural directives.abpBody() returns undefined but the modal is visible — this would help developers diagnose the issue faster.package.json){
"@angular/core": "~22.1.4",
"@angular/material": "~22.1.4",
"@angular/cdk": "~22.1.4",
"@abp/ng.theme.shared": "~10.6.0",
"@abp/ng.core": "~10.6.0",
"@ng-bootstrap/ng-bootstrap": "^21.0.0",
"ng-zorro-antd": "^22.0.1"
}
@abp/ng.theme.shared v10.6.0 bundles its own @ng-bootstrap/ng-bootstrap v20.0.0.
abp-ng.theme.shared.mjs line 1775: contentChild('abpBody') signal queryabp-ng.theme.shared.mjs line 1826: toggle() spreads options() to ng-bootstrapng-bootstrap-ng-bootstrap-modal.mjs line 83: Backdrop style: 'z-index: 1055' hardcodedTested in Chrome 130+ and Edge 130+. Issue is CSS z-index stacking, not browser-specific.
Open a component via this.dialog.open(AlertComponent, { data, width: '80%' }). Inside that MatDialog component:
<abp-modal [(visible)]="openViewDocument">
<ng-template #abpBody><p>Content here</p></ng-template>
</abp-modal>
Result: abp-modal renders behind the MatDialog overlay — invisible to the user.
<abp-modal [(visible)]="showModal">
<ng-template *ngIf="isEditMode" #abpBody>
<app-edit-form></app-edit-form>
</ng-template>
<ng-template *ngIf="!isEditMode" #abpBody>
<app-view-only></app-view-only>
</ng-template>
</abp-modal>
Result: Modal opens but body is empty — contentChild('abpBody') returns undefined.
| # | Request | Severity |
|---|---------|----------|
| 1 | Document the contentChild breaking change for *ngIf on <ng-template #abpBody> in migration guide | Critical |
| 2 | Add z-index configuration to ModalComponent (e.g., zIndex input or CSS custom property) | High |
| 3 | Document windowClass/backdropClass passthrough via [options] input | High |
| 4 | Add runtime warning when abpBody() signal returns undefined but modal is visible | Medium |
| 5 | Consider CDK overlay for modal rendering instead of ng-bootstrap to avoid z-index conflicts with other CDK-based components | Low (long-term) |
Reported by: TASConnect Platform Team Contact: Pradeep Pradyumna Attachments: Screenshots available from JIRA tickets VL-246XX and VL-246XX
I'm looking to upgrade the Angular with ABP which is at Angular 17 and ABP 8.3.0 versions. I get a lot of issues, when I did using it Claude. Here are the reports:
Present scenario: Currently our solution, we are using three microservices say ms1, ms2 and ms3 and one API gateway project. We are injecting EntityFrameworkCoreModule and ContractsModule of all 3 microservices inside gateway. Inside dependson[] we have 3 pairs of EntityFrameworkCoreModule and ContractsModule along with other abp dependencies.
Problem statement: We're building a new module that requires a new microservices ms4. Now, we only want inject dependencies of ms4 to gateway, and remove references of ms1, ms2 and ms3. Doing so, gives us an error since these gateway requires references of ms1, ms2 and ms3 to enable its features/ permissions at the time of application load.
Expectation: I want build and deploy my solution containing just gateway and ms4. I don't want to deploy ms1, ms2 and ms3. How can I inject ms4 alone? If it really required to inject ms1, ms2 and ms3, can I do it conditionally?