A lightweight, standalone, SSR-safe modal system for Angular 17+.
Provides a simple service-based API for rendering any standalone component as a modal.
Designed to be headless, predictable, and framework-aligned (Signals-friendly).
- Angular 17+ standalone component support
- Fully tested
- Simple
ModalService.open()API - Pass data into modal components via
@Input() - Signal-friendly data assignment (
{ data: ... }supported) - Strongly typed modal results via
ModalRef<TResult> - Optional backdrop
- Optional ESC-to-close behavior
- Optional body scroll locking
- Programmatic close support
- Idempotent cleanup (
close()is safe to call multiple times) - SSR-safe:
- queues modal opens on the server
- flushes automatically on the client once stable
npm install modal-lib2import { Component, inject, Input } from '@angular/core';
import { ModalRef } from 'modal-lib2';
export interface LoginResult {
success: boolean;
token?: string;
}
@Component({
selector: 'app-login-modal',
standalone: true,
template: `
<h3>Login</h3>
<p>Hello, {{ username }}</p>
<button (click)="ok()">OK</button>
<button (click)="cancel()">Cancel</button>
`,
})
export class LoginModalComponent {
@Input() username = '';
private modalRef = inject<ModalRef<LoginResult>>(ModalRef);
ok() {
this.modalRef.close({ success: true, token: '123' });
}
cancel() {
this.modalRef.close({ success: false });
}
}import { Component, inject } from '@angular/core';
import { ModalService } from 'modal-lib2';
import { LoginModalComponent, LoginResult } from './login-modal.component';
@Component({
selector: 'app-root',
standalone: true,
template: `<button (click)="open()">Open Login</button>`,
})
export class AppComponent {
private modal = inject(ModalService);
open() {
const { ref, instance } =
this.modal.open<LoginModalComponent, LoginResult>(
LoginModalComponent,
{ username: 'Burt' },
{
backdrop: true,
closeOnEsc: true,
lockScroll: true,
}
);
ref.afterClosed.subscribe(result => {
console.log('Modal closed with:', result);
});
console.log('Modal instance:', instance);
}
}component
Standalone component to render inside the modal.
data
Partial object assigned to the component instance.
Supports both styles:
{ username: 'burt' }signal friendly
{ data: { stuff :{moreStuff:{hi:'there'}} } }options*
export type ModalOptions = {
backdrop?: boolean; // default: true
closeOnEsc?: boolean; // default: true
lockScroll?: boolean; // default: false
};retrun value
{
ref: ModalRef<TResult>;
instance: T | undefined;
close: () => void;
}
- instance is undefined when called on the server
- close() is always safe to callclass ModalRef<TResult> {
afterClosed: Observable<TResult | undefined>;
close(result?: TResult): void;
}
**note**
- afterClosed emits once and then completes
- close() is idempotent
- Calling close() multiple times has no additional effectopen()queues the modal request- No DOM access occurs
- Queued modals render automatically once Angular becomes stable
- Nothing is rendered on the client
-
Modal options are non-sticky
Eachopen()call is evaluated independently. -
Defaults are explicit and predictable:
- Scroll is not locked unless requested
- ESC closing is opt-in per modal
-
The library is intentionally headless:
- No styles, animations, or layout opinions imposed
- Service-based modal API (
ModalService.open) - Strongly typed modal results
- Signal-friendly data handling
- ESC, backdrop, and scroll-lock options
- Idempotent cleanup and leak-free lifecycle
- Fully SSR-safe behavior