A lightweight Angular modal component with signal-based reactivity and full standalone support
- 🎯 Angular Native - Built specifically for Angular 17+
- 🎭 Standalone Components - No NgModule required
- 🚀 Signal-Based Reactivity - Optimized performance with Angular signals
- 🔗 Advanced Modal Stacking - Intelligent z-index management and focus trapping
- 🎨 Bootstrap 3, 4 & 5 Compatible - Seamless integration with all Bootstrap versions
- 📱 Mobile Friendly - Touch-optimized for mobile devices
- ⚡ Lightweight - Minimal bundle impact with performance optimization
- 🔧 TypeScript - Full type safety and IntelliSense
- 🎪 Custom Content - Support for components and HTML templates
- 🎛️ Configurable - Extensive customization options
- ♿ WCAG Compliant - Full accessibility support with screen reader compatibility
- 🔄 Route Aware - Automatic modal closing on navigation (configurable)
⚠️ IMPORTANT: Version 20.0.0 introduces significant breaking changes✅ Full Angular 17, 18, 19 & 20 Compatibility
Before upgrading, please review the CHANGELOG.md for detailed migration instructions
- 🎨 Complete CSS/SCSS Rewrite - New custom properties system and modern styling
- 🏗️ Enhanced HTML Structure - Improved accessibility with ARIA attributes
- ⚡ Signal-Based Architecture - Performance optimizations with Angular signals
- 🔧 New APIs & Services - Modal stack management and lifecycle compatibility
-
🎯 Z-Index Management - Updated from
z-index: 42
to Bootstrap-standard1050+
- 🔄 Route Change Detection - Automatic modal closing on navigation
Component | What Changed | Action Required |
---|---|---|
Custom CSS | Complete styling overhaul | Review and update custom styles |
Event Handling | New granular events (opening , opened , closing , closed ) |
Update event listeners |
Testing | New signal-based testing patterns | Update test assertions |
Accessibility | Enhanced ARIA attributes | Verify accessibility implementations |
Route Behavior | Automatic modal closing on navigation (enabled by default) | Configure if different behavior needed |
👉 Read Full Migration Guide in CHANGELOG.md
npm install ngx-custom-modal
# or
yarn add ngx-custom-modal
# or
pnpm add ngx-custom-modal
import { NgxCustomModalComponent } from 'ngx-custom-modal';
@Component({
selector: 'app-example',
standalone: true,
imports: [NgxCustomModalComponent],
template: `
<button (click)="modal.open()">Open Modal</button>
<ngx-custom-modal #modal>
<ng-template #modalHeader>
<h2>Modal Title</h2>
</ng-template>
<ng-template #modalBody>
<p>Modal content goes here!</p>
</ng-template>
</ngx-custom-modal>
`,
})
export class ExampleComponent {}
import { Component, signal } from '@angular/core';
import { NgxCustomModalComponent, ModalOptions } from 'ngx-custom-modal';
@Component({
selector: 'app-signal-example',
standalone: true,
imports: [NgxCustomModalComponent],
template: `
<button (click)="modal.open()">Open Signal Modal</button>
<ngx-custom-modal
#modal
[size]="modalSize()"
[options]="modalOptions()"
(opened)="onModalOpened()"
(closed)="onModalClosed()"
>
<ng-template #modalHeader>
<h2>{{ modalTitle() }}</h2>
</ng-template>
<ng-template #modalBody>
@if (showContent()) {
<p>{{ modalContent() }}</p>
@for (item of items(); track item.id) {
<div class="item">{{ item.name }}</div>
}
}
</ng-template>
</ngx-custom-modal>
`,
})
export class SignalExampleComponent {
modalTitle = signal('Signal-Based Modal');
modalContent = signal('This modal uses Angular 17 features!');
modalSize = signal<'sm' | 'md' | 'lg' | 'xl'>('lg');
showContent = signal(true);
items = signal([
{ id: 1, name: 'Signal-based reactivity' },
{ id: 2, name: 'Control flow syntax' },
{ id: 3, name: 'Performance optimization' },
]);
modalOptions = signal<ModalOptions>({
closeOnOutsideClick: true,
closeOnEscape: true,
animation: true,
centered: true,
closeOnRouteChange: true, // Default: true - automatically close on navigation
});
onModalOpened() {
console.log('Modal opened with signals!');
}
onModalClosed() {
console.log('Modal closed');
}
}
@Component({
template: `
<button (click)="componentModal.open()">Open Component Modal</button>
<ngx-custom-modal #componentModal [size]="'lg'">
<ng-template #modalHeader>
<h2>Component Modal</h2>
</ng-template>
<ng-template #modalBody>
<app-my-component [data]="componentData"></app-my-component>
</ng-template>
<ng-template #modalFooter>
<button (click)="componentModal.close()" class="btn btn-secondary">Close</button>
</ng-template>
</ngx-custom-modal>
`,
})
export class ComponentModalExample {
componentData = { message: 'Hello from component!' };
}
@Component({
template: `
<ngx-custom-modal #parentModal [size]="'xl'">
<ng-template #modalHeader>
<h2>Parent Modal</h2>
</ng-template>
<ng-template #modalBody>
<p>This is the parent modal with automatic stack management.</p>
<button (click)="childModal.open()">Open Child Modal</button>
<ngx-custom-modal #childModal [size]="'md'" [centered]="true">
<ng-template #modalHeader>
<h3>Child Modal</h3>
</ng-template>
<ng-template #modalBody>
<p>This is a nested modal with proper z-index handling!</p>
</ng-template>
</ngx-custom-modal>
</ng-template>
</ngx-custom-modal>
`,
})
export class NestedModalExample {}
@Component({
template: `
<ngx-custom-modal
#customModal
[closeOnOutsideClick]="false"
[closeOnEscape]="false"
[hideCloseButton]="true"
[size]="'lg'"
[centered]="true"
[scrollable]="true"
[animation]="true"
[closeOnRouteChange]="false"
customClass="my-custom-modal"
>
<ng-template #modalHeader>
<h2>Custom Modal</h2>
</ng-template>
<ng-template #modalBody>
<p>This modal has custom configuration!</p>
<p>It will NOT close automatically when navigating to another route.</p>
<button (click)="customModal.close()">Manual Close</button>
</ng-template>
</ngx-custom-modal>
`,
})
export class CustomModalExample {}
@Component({
template: `
<ngx-custom-modal #optionsModal [options]="modalOptions">
<ng-template #modalHeader>
<h2>Options Modal</h2>
</ng-template>
<ng-template #modalBody>
<p>Configured via options object</p>
</ng-template>
</ngx-custom-modal>
`,
})
export class OptionsModalExample {
modalOptions: ModalOptions = {
closeOnOutsideClick: false,
closeOnEscape: true,
customClass: 'my-modal-class',
hideCloseButton: false,
size: 'lg',
centered: true,
scrollable: false,
animation: true,
animationDuration: 300,
backdrop: 'dynamic',
keyboard: true,
focus: true,
closeOnRouteChange: true,
};
}
@Component({
template: `
<!-- Modal that stays open during navigation -->
<ngx-custom-modal #persistentModal [closeOnRouteChange]="false">
<ng-template #modalHeader>
<h2>Persistent Modal</h2>
</ng-template>
<ng-template #modalBody>
<p>This modal will remain open even when you navigate to other pages.</p>
<p>Useful for shopping carts, music players, or global notifications.</p>
</ng-template>
</ngx-custom-modal>
<!-- Modal that closes on navigation (default behavior) -->
<ngx-custom-modal #standardModal>
<ng-template #modalHeader>
<h2>Standard Modal</h2>
</ng-template>
<ng-template #modalBody>
<p>This modal will automatically close when navigating to other pages.</p>
<p>This is the default behavior for better UX.</p>
</ng-template>
</ngx-custom-modal>
`,
})
export class RouteAwareModalExample {}
Property | Type | Default | Description |
---|---|---|---|
closeOnOutsideClick |
boolean |
true |
Close modal when clicking outside |
closeOnEscape |
boolean |
true |
Close modal when pressing Escape key |
customClass |
string |
'' |
Custom CSS class for the modal |
hideCloseButton |
boolean |
false |
Hide the default close button |
options |
ModalOptions |
{} |
Configuration options object |
size |
'sm' | 'md' | 'lg' | 'xl' |
'md' |
Modal size |
centered |
boolean |
false |
Center modal vertically |
scrollable |
boolean |
false |
Make modal body scrollable |
animation |
boolean |
true |
Enable/disable animations |
backdrop |
'static' | 'dynamic' |
'dynamic' |
Backdrop behavior |
keyboard |
boolean |
true |
Enable keyboard interactions |
focus |
boolean |
true |
Enable focus management |
closeOnRouteChange |
boolean |
true |
Close modal on route navigation |
Event | Type | Description |
---|---|---|
opening |
EventEmitter<void> |
Emitted when modal starts opening |
opened |
EventEmitter<void> |
Emitted when modal is fully opened |
closing |
EventEmitter<void> |
Emitted when modal starts closing |
closed |
EventEmitter<void> |
Emitted when modal is fully closed |
Template Ref | Type | Description |
---|---|---|
#modalHeader |
TemplateRef |
Header content template |
#modalBody |
TemplateRef |
Body content template |
#modalFooter |
TemplateRef |
Footer content template |
Method | Returns | Description |
---|---|---|
open() |
void |
Opens the modal |
close() |
void |
Closes the modal |
toggle() |
void |
Toggles modal visibility |
isTopMost() |
boolean |
Checks if modal is topmost |
interface ModalOptions {
closeOnOutsideClick?: boolean;
closeOnEscape?: boolean;
customClass?: string;
hideCloseButton?: boolean;
backdrop?: 'static' | 'dynamic';
keyboard?: boolean;
focus?: boolean;
size?: 'sm' | 'md' | 'lg' | 'xl';
centered?: boolean;
scrollable?: boolean;
animation?: boolean;
animationDuration?: number;
closeOnRouteChange?: boolean;
}
By default, modals will automatically close when the user navigates to a different route. This provides a better user experience and prevents modals from appearing in unexpected contexts.
// Default behavior - modal closes on navigation
<ngx-custom-modal #modal>
<!-- Modal content -->
</ngx-custom-modal>
// Explicitly enabled
<ngx-custom-modal #modal [closeOnRouteChange]="true">
<!-- Modal content -->
</ngx-custom-modal>
For specific use cases where you want the modal to persist across route changes (shopping carts, media players, global notifications), you can disable this behavior:
// Modal persists across route changes
<ngx-custom-modal #modal [closeOnRouteChange]="false">
<!-- Modal content -->
</ngx-custom-modal>
// Via options object
modalOptions: ModalOptions = {
closeOnRouteChange: false,
// other options...
};
If you're upgrading from a previous version and have modals that were designed to persist across routes, you'll need to explicitly set closeOnRouteChange: false
:
// Before v20.0.0 (modals persisted by default)
<ngx-custom-modal #modal>
<!-- Modal content -->
</ngx-custom-modal>
// After v20.0.0 (to maintain same behavior)
<ngx-custom-modal #modal [closeOnRouteChange]="false">
<!-- Modal content -->
</ngx-custom-modal>
The library comes with modern CSS custom properties for easy theming:
:root {
/* Modal backdrop */
--modal-backdrop-bg: rgba(0, 0, 0, 0.5);
--modal-backdrop-blur: 2px;
/* Modal content */
--modal-content-bg: #fff;
--modal-content-border: 1px solid rgba(0, 0, 0, 0.125);
--modal-content-border-radius: 0.5rem;
--modal-content-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
/* Animations */
--modal-animation-duration: 200ms;
--modal-z-index: 1050;
}
/* Basic modal styles */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
min-height: 100%;
background-color: var(--modal-backdrop-bg);
z-index: var(--modal-z-index);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity var(--modal-animation-duration) ease-in-out;
backdrop-filter: blur(var(--modal-backdrop-blur));
}
.modal.in {
opacity: 1;
}
.modal-content {
background-color: var(--modal-content-bg);
border: var(--modal-content-border);
border-radius: var(--modal-content-border-radius);
box-shadow: var(--modal-content-shadow);
max-width: 500px;
width: 90%;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #dee2e6;
}
.modal-body {
padding: 1rem;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
padding: 0.75rem;
border-top: 1px solid #dee2e6;
}
.close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
padding: 0.25rem;
opacity: 0.5;
transition: opacity 0.15s ease-in-out;
}
.close:hover {
opacity: 0.75;
}
For Bootstrap users, ngx-custom-modal works seamlessly with all Bootstrap versions:
<!-- Bootstrap Modal Example -->
<ngx-custom-modal #bootstrapModal [size]="'lg'" [centered]="true">
<ng-template #modalHeader>
<h1 class="modal-title fs-5">Bootstrap Modal</h1>
</ng-template>
<ng-template #modalBody>
<div class="container-fluid">
<div class="row">
<div class="col-md-6">
<p class="text-muted">Left column content</p>
</div>
<div class="col-md-6">
<p class="text-muted">Right column content</p>
</div>
</div>
</div>
</ng-template>
<ng-template #modalFooter>
<button type="button" class="btn btn-secondary" (click)="bootstrapModal.close()">Close</button>
<button type="button" class="btn btn-primary">Save changes</button>
</ng-template>
</ngx-custom-modal>
/* Dark mode support */
@media (prefers-color-scheme: dark) {
:root {
--modal-backdrop-bg: rgba(0, 0, 0, 0.8);
--modal-content-bg: #1f2937;
--modal-content-border: 1px solid #374151;
--modal-text-color: #f9fafb;
}
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
:root {
--modal-animation-duration: 0ms;
}
}
/* High contrast mode */
@media (prefers-contrast: high) {
:root {
--modal-content-border: 2px solid currentColor;
--modal-backdrop-bg: rgba(0, 0, 0, 0.9);
}
}
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
- Mobile browsers (iOS Safari, Chrome Mobile)
- Node.js 18+
- Angular CLI 17+
git clone https://github.com/AngelCareaga/ngx-custom-modal.git
cd ngx-custom-modal
npm install
npm start
npm run build:lib
npm test
This project uses Prettier for code formatting:
npm run format
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature
) - Commit your Changes (
git commit -m 'Add some AmazingFeature'
) - Push to the Branch (
git push origin feature/AmazingFeature
) - Open a Pull Request
- Follow the existing code style
- Add tests for new features
- Update documentation for any API changes
- Use conventional commit messages
This project is licensed under the MIT License - see the LICENSE file for details.
- angular-custom-modal - Created by Gerard Rovira Sánchez, which served as inspiration for this project
- Stephen Paul - For the initial Angular 2 Modal concept
- Angular Team - For the amazing framework
- 📧 Email: dev.angelcareaga@gmail.com
- 🐛 Issues: GitHub Issues
- 💬 Discussions: GitHub Discussions
- 🌐 Website: angelcareaga.com
Made with ❤️ by Angel Careaga
⭐ Star this repo if you found it helpful!