A custom Modal / Dialog (with inner component support) for Angular 17-20 projects with full version compatibility
npm install ngx-custom-modal





A lightweight Angular modal component with signal-based reactivity and full standalone support
Live Examples β’ Documentation β’ Report Bug
- π― 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-standard 1050+
- π 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
---
``bash`
npm install ngx-custom-modalor
yarn add ngx-custom-modalor
pnpm add ngx-custom-modal
`typescript
import { NgxCustomModalComponent } from 'ngx-custom-modal';
@Component({
selector: 'app-example',
standalone: true,
imports: [NgxCustomModalComponent],
template:
Modal content goes here!
Modal Title
,`
})
export class ExampleComponent {}
`typescript
import { Component, signal } from '@angular/core';
import { NgxCustomModalComponent, ModalOptions } from 'ngx-custom-modal';
@Component({
selector: 'app-signal-example',
standalone: true,
imports: [NgxCustomModalComponent],
template:
[size]="modalSize()"
[options]="modalOptions()"
(opened)="onModalOpened()"
(closed)="onModalClosed()"
>
{{ modalTitle() }}
{{ modalContent() }}
@if (showContent()) {
@for (item of items(); track item.id) {
{{ item.name }}
}
}
,
})
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
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');
}
}
`
`typescript
@Component({
template:
Component Modal
,`
})
export class ComponentModalExample {
componentData = { message: 'Hello from component!' };
}
` This is the parent modal with automatic stack management.typescript
@Component({
template:
Parent Modal
This is a nested modal with proper z-index handling!
Child Modal
,`
})
export class NestedModalExample {}
` This modal has custom configuration! It will NOT close automatically when navigating to another route.typescript
@Component({
template:
[closeOnOutsideClick]="false"
[closeOnEscape]="false"
[hideCloseButton]="true"
[size]="'lg'"
[centered]="true"
[scrollable]="true"
[animation]="true"
[closeOnRouteChange]="false"
customClass="my-custom-modal"
>
Custom Modal
,`
})
export class CustomModalExample {}
` Configured via options objecttypescript
@Component({
template:
Options 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,
};
}
` This modal will remain open even when you navigate to other pages. Useful for shopping carts, music players, or global notifications.typescript
@Component({
template:
Persistent Modal
This modal will automatically close when navigating to other pages. This is the default behavior for better UX.
Standard 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 | Emitted when modal starts opening |opened
| | EventEmitter | Emitted when modal is fully opened |closing
| | EventEmitter | Emitted when modal starts closing |closed
| | EventEmitter | 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 |
`typescript`
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.
`typescript
// Default behavior - modal closes on navigation
// Explicitly enabled
`
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:
`typescript
// Modal persists across route changes
// 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:
`typescript
// Before v20.0.0 (modals persisted by default)
// After v20.0.0 (to maintain same behavior)
`
The library comes with modern CSS custom properties for easy theming:
`css
: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:
` Left column content Right column contenthtml`
Bootstrap Modal
`css
/ 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+
`bash`
git clone https://github.com/AngelCareaga/ngx-custom-modal.git
cd ngx-custom-modal
npm install
`bash`
npm start
`bash`
npm run build:lib
`bash`
npm test
This project uses Prettier for code formatting:
`bash`
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.
1. Fork the Project
2. Create your Feature Branch (git checkout -b feature/AmazingFeature)git commit -m 'Add some AmazingFeature'
3. Commit your Changes ()git push origin feature/AmazingFeature`)
4. Push to the Branch (
5. 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!