@severfam/modals
v0.1.0
Published
Angular modal window library with @ngrx/component-store state management
Maintainers
Readme
Modals
Angular 21 modal window library with centralized state management via @ngrx/component-store.
Features
- Standalone component — no
NgModulerequired - Signal-based inputs (
input(),output(),contentChild()) - Centralized state via
@ngrx/component-store(singleton,providedIn: 'root') - Configurable animations (duration, disable entirely)
- Backdrop click-to-close, Escape key support
- Full-screen and full-mode layouts
- Configurable z-index for backdrop and dialog
moveToBodydirective to avoid z-index/overflow issues- CSS custom properties for deep theming
- BEM class naming convention
ng-templatecontent projection (#modalHeader,#modalBody,#modalFooter,#closeButton,#backdrop)
Installation
npm install @severfam/modals @ngrx/component-storePeer Dependencies
| Package | Version |
|---------|---------|
| @angular/core | ^21.2.0 |
| @angular/common | ^21.2.0 |
| @ngrx/component-store | ^21.0.0 |
Quick Start
import { Component, inject } from '@angular/core';
import { ModalWindowComponent, ModalWindowStore } from '@severfam/modals';
@Component({
standalone: true,
imports: [ModalWindowComponent],
template: `
<button (click)="openModal()">Open</button>
<modal-window
[uid]="modalId"
[closeOnBackdropClick]="true"
[maxWidth]="'420px'"
(selfClose)="onModalClose()"
>
<ng-template #modalHeader>
<span>Confirm</span>
</ng-template>
<ng-template #modalBody>
<p>Are you sure you want to proceed?</p>
</ng-template>
<ng-template #modalFooter>
<button (click)="closeModal()">Cancel</button>
<button (click)="confirm()">OK</button>
</ng-template>
</modal-window>
`,
})
export class MyComponent {
private readonly modalStore = inject(ModalWindowStore);
readonly modalId = 'confirm-modal';
openModal() {
this.modalStore.open(this.modalId);
}
closeModal() {
this.modalStore.close(this.modalId);
}
onModalClose() {
console.log('Modal closed');
}
confirm() {
// handle confirm
this.closeModal();
}
}API Reference
ModalWindowComponent
Selector: modal-window
Inputs
| Input | Type | Default | Description |
|-------|------|---------|-------------|
| uid | string | required | Unique identifier for the modal |
| maxWidth | number \| string | '500px' | Max width (number = px, string = CSS value) |
| closeOnBackdropClick | boolean | false | Close when clicking the backdrop |
| fullMode | boolean | false | Full-screen on mobile (< 768px) |
| fullScreen | boolean | false | Always full-screen |
| hiddenMode | boolean | false | Keep in DOM, hide via visibility |
| openOnCreate | boolean | false | Auto-open on initialization |
| showCloseButton | boolean | true | Show the close button |
| showBackdrop | boolean | true | Show the backdrop overlay |
| animationDuration | number | 200 | Animation duration in ms |
| disableAnimation | boolean | false | Disable animations entirely |
| backdropZIndex | number | 1351 | Z-index for the backdrop |
| dialogZIndex | number | 1352 | Z-index for the dialog |
| moveToBody | 'begin' \| 'end' \| 'stay' | 'end' | Where to append the backdrop in <body> |
Outputs
| Output | Type | Description |
|--------|------|-------------|
| selfClose | EventEmitter<void> | Emitted when the modal closes |
Content Projection (ng-template refs)
| Template Ref | Description |
|-------------|-------------|
| #modalHeader | Modal header |
| #modalBody | Modal body |
| #modalFooter | Modal footer |
| #closeButton | Custom close button |
| #backdrop | Custom backdrop |
ModalWindowStore
Singleton store (providedIn: 'root'). Manages open/close state, params, and action history.
State Shape
interface ModalWindowState {
modals: Record<string, boolean>; // { [uid]: isOpen }
params: Record<string, unknown>; // { [uid]: params }
action: ModalAction; // last action
}Methods
| Method | Description |
|--------|-------------|
| open(key) | Open a modal by uid |
| openWithParams({ key, params }) | Open with typed params |
| close(key) | Close a modal by uid |
| patch(payload) | Batch update { [uid]: boolean } |
| reset() | Close all modals |
| isOpen(key) | Check state (sync) |
| isOpen$(key) | Check state (Observable) |
| getParams<T>(key) | Get params with type <T> (sync) |
| getParams$<T>(key) | Get params with type <T> (Observable) |
Usage Examples
Basic Modal
@Component({
standalone: true,
imports: [ModalWindowComponent],
template: `
<button (click)="store.open('basic')">Open</button>
<modal-window
[uid]="'basic'"
[closeOnBackdropClick]="true"
[maxWidth]="'500px'"
(selfClose)="store.close('basic')"
>
<ng-template #modalHeader><span>Hello</span></ng-template>
<ng-template #modalBody><p>Basic modal with backdrop.</p></ng-template>
<ng-template #modalFooter>
<button (click)="store.close('basic')">Close</button>
</ng-template>
</modal-window>
`,
})
export class BasicExample {
readonly store = inject(ModalWindowStore);
}Modal with Params
@Component({
standalone: true,
imports: [ModalWindowComponent],
template: `
<button (click)="openWithParams()">Open with data</button>
<modal-window
[uid]="'params-modal'"
[closeOnBackdropClick]="true"
[maxWidth]="'400px'"
(selfClose)="store.close('params-modal')"
>
<ng-template #modalHeader><span>Details</span></ng-template>
<ng-template #modalBody>
@if (store.getParams<{ name: string }>('params-modal'); as params) {
<p>Name: {{ params.name }}</p>
}
</ng-template>
</modal-window>
`,
})
export class ParamsExample {
readonly store = inject(ModalWindowStore);
openWithParams() {
this.store.openWithParams({
key: 'params-modal',
params: { name: 'Angular' },
});
}
}Full-Screen Modal
<modal-window
[uid]="'fullscreen'"
[fullScreen]="true"
[closeOnBackdropClick]="true"
(selfClose)="store.close('fullscreen')"
>
<ng-template #modalHeader><span>Full Screen</span></ng-template>
<ng-template #modalBody><p>Takes the entire viewport.</p></ng-template>
</modal-window>Modal with Custom Z-Index
<modal-window
[uid]="'z-index-modal'"
[closeOnBackdropClick]="true"
[maxWidth]="'400px'"
[backdropZIndex]="2000"
[dialogZIndex]="2001"
(selfClose)="store.close('z-index-modal')"
>
<ng-template #modalHeader><span>Custom Z-Index</span></ng-template>
<ng-template #modalBody><p>Backdrop: 2000, Dialog: 2001.</p></ng-template>
</modal-window>Modal without Backdrop
<modal-window
[uid]="'no-backdrop'"
[showBackdrop]="false"
[closeOnBackdropClick]="true"
[maxWidth]="'320px'"
(selfClose)="store.close('no-backdrop')"
>
<ng-template #modalHeader><span>No Overlay</span></ng-template>
<ng-template #modalBody><p>Dialog without backdrop overlay.</p></ng-template>
</modal-window>Custom Close Button
<modal-window [uid]="'custom-close'" [showCloseButton]="true">
<ng-template #modalHeader><span>Title</span></ng-template>
<ng-template #closeButton>
<button class="my-close" (click)="closeModal()">X</button>
</ng-template>
<ng-template #modalBody><p>Content</p></ng-template>
</modal-window>Custom Backdrop
<modal-window [uid]="'custom-backdrop'">
<ng-template #backdrop>
<div class="my-backdrop" (click)="closeModal()">
<div class="my-dialog">
<ng-container *ngTemplateOutlet="dialogContent"></ng-container>
</div>
</div>
</ng-template>
</modal-window>Hidden Mode (Stay in DOM)
<modal-window
[uid]="'hidden-mode'"
[hiddenMode]="true"
[closeOnBackdropClick]="true"
[maxWidth]="'400px'"
(selfClose)="store.close('hidden-mode')"
>
<ng-template #modalHeader><span>Hidden Mode</span></ng-template>
<ng-template #modalBody>
<p>Stays in DOM when closed (visibility: hidden).</p>
</ng-template>
</modal-window>Programmatic Control from Any Component
import { inject } from '@angular/core';
import { ModalWindowStore } from '@severfam/modals';
@Component({ ... })
export class AnyComponent {
private readonly store = inject(ModalWindowStore);
openConfirmDialog() {
this.store.openWithParams({
key: 'confirm',
params: { message: 'Delete this item?' },
});
}
isConfirmOpen() {
return this.store.isOpen('confirm');
}
closeConfirmDialog() {
this.store.close('confirm');
}
closeAll() {
this.store.reset();
}
}Styling
BEM Classes
| Class | Description |
|-------|-------------|
| modal__backdrop | Backdrop overlay |
| modal__dialog | Dialog container |
| modal__content | Content wrapper |
| modal__header | Header section |
| modal__title | Title wrapper |
| modal__close | Close button |
| modal__body | Body section |
| modal__footer | Footer section |
Modifiers
| Class | Description |
|-------|-------------|
| modal__backdrop--hidden | Hidden state (visibility) |
| modal__backdrop--entering | Fade-in animation |
| modal__backdrop--leaving | Fade-out animation |
| modal__backdrop--full-screen | Full-screen backdrop |
| modal__dialog--entering | Slide-up animation |
| modal__dialog--leaving | Slide-down animation |
| modal__dialog--full-mode | Full-screen on mobile |
| modal__dialog--full-screen | Always full-screen |
CSS Custom Properties
| Variable | Default | Description |
|----------|---------|-------------|
| --modal-backdrop-bg | rgba(0, 0, 0, 0.5) | Backdrop color |
| --modal-dialog-bg | #ffffff | Dialog background |
| --modal-dialog-border-radius | 10px | Dialog border radius |
| --modal-dialog-shadow | 0 7px 22px rgba(50, 50, 50, 0.3) | Dialog shadow |
| --modal-dialog-padding | 30px | Dialog padding |
| --modal-header-font-size | 18px | Header font size |
| --modal-header-font-weight | 600 | Header font weight |
| --modal-header-color | #333333 | Header color |
| --modal-header-font-family | 'Open Sans', sans-serif | Header font family |
| --modal-header-margin-bottom | 20px | Header bottom margin |
| --modal-close-color | #cc0000 | Close button color |
| --modal-close-hover-color | #990000 | Close button hover color |
| --modal-footer-margin-top | 20px | Footer top margin |
| --modal-animation-duration | 200ms | Animation duration |
Theming Examples
/* Global override */
:root {
--modal-dialog-bg: #f8f8f8;
--modal-backdrop-bg: rgba(0, 0, 0, 0.8);
--modal-header-color: #000000;
--modal-close-color: #666666;
}
/* Scoped override */
modal-window {
--modal-dialog-border-radius: 20px;
--modal-dialog-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
}Animations
- Backdrop: fade-in/out (opacity 0 → 1)
- Dialog: slide-up (translateY(20px) → 0) + fade
Duration is controlled by animationDuration input (ms) or --modal-animation-duration CSS variable. Set disableAnimation to true to disable all animations.
Development
# Build the library
ng build modals
# Run tests
ng test modals
# Lint
ng lint
# Format
npm run formatLicense
MIT
