A lightweight, zero-dependency Angular library for managing dialogs using the native HTML <dialog> API. Accessible, customizable, and fully driven by CSS animations.
Requires Angular 16 or newer
Install the library:
npm install ngx-zero-dialog
# or
yarn add ngx-zero-dialogAdd a dialog container to your root component template (e.g. app.component.html):
<div id="ngx-zero-dialog-container"></div>Provide the configuration in your app config:
import { provideNgxZeroDialog } from 'ngx-zero-dialog';
export const appConfig: ApplicationConfig = {
providers: [
provideNgxZeroDialog({ containerNodeID: 'ngx-zero-dialog-container' }),
],
};Import the library styles in your global stylesheet:
@import "ngx-zero-dialog/styles/ngx-zero-dialog.scss";Open a dialog:
import { NgxZeroDialogService } from 'ngx-zero-dialog';
@Component({ ... })
export class MyComponent {
private readonly dialog = inject(NgxZeroDialogService);
open(template: TemplateRef<unknown>) {
this.dialog.openDialog(template).subscribe((result) => {
console.log('Dialog closed with:', result);
});
}
}<button (click)="open(myDialog)">Open</button>
<ng-template #myDialog let-dialogRef>
<p>Hello from a dialog!</p>
<button (click)="dialogRef.close('done')">Close</button>
</ng-template>That's it — no custom host component needed. A built-in default host with a close button is used automatically.
NgModule setup: If you're not using standalone APIs, add
provideNgxZeroDialog(...)to your module'sprovidersarray instead.
Each dialog consists of three elements:
- Native
<dialog>element — created automatically, provides browser-level modal behavior (focus trapping, backdrop, stacking context) - Host component — the dialog's frame/chrome (title bar, close button, layout). A built-in default is provided, or you can create your own.
- Content — your actual dialog body, passed as a
ComponentorTemplateRef
HTML dialogs exist in their own top-layer, ensuring that dialogs and their child views never intersect regardless of z-index or how nodes are positioned. No overlay system or CDK is needed.
openDialog() returns a cold Observable — the dialog is only created when subscribed to. The observable emits a single result value when the dialog closes, then completes.
The main service. Has one public method:
openDialog<Result>(
componentOrTemplate: Component | TemplateRef<any>,
config?: IDialogConfig,
): Observable<DialogResult<Result>>All fields are optional:
interface IDialogConfig {
/** Close when clicking outside the dialog. Default: true */
closeOnBackdropClick?: boolean;
/** Close when pressing Escape. Default: true */
closeOnEsc?: boolean;
/**
* Host component wrapping the dialog content.
* If omitted, a built-in DefaultDialogHostComponent is used.
*/
hostComponent?: Component;
/** Data injected into the host component via HOST_DATA token */
hostData?: IHostData;
/** CSS class(es) added to the native <dialog> element */
dialogNodeClass?: string | string[];
/** Data injected into the content via DIALOG_DATA token */
dialogData?: IDialogData;
/** Enable open/close CSS transitions. Default: true */
animated?: boolean;
}A reference to an open dialog, available in both host and content components via the DIALOG_REF token.
class DialogRef<Result = unknown> {
/** The underlying native <dialog> element */
readonly nativeDialog: HTMLDialogElement;
/** Observable that emits the result when the dialog closes */
readonly closed$: Observable<DialogResult<Result>>;
/** Close the dialog, optionally emitting a result value */
close(value?: DialogResult<Result>): void;
/**
* Register a guard that is called before close.
* Return false (or Observable<false> / Promise<false>) to prevent closing.
*/
beforeClose(guard: BeforeCloseGuard<Result>): void;
}DialogResult<Result> is Result | undefined — undefined is emitted when the dialog closes without a value.
type BeforeCloseGuard<Result> = (
value?: DialogResult<Result>
) => boolean | Observable<boolean> | Promise<boolean>;Example — confirm before closing:
@Component({ ... })
export class MyDialog {
private readonly dialogRef = inject<DialogRef<string>>(DIALOG_REF);
constructor() {
this.dialogRef.beforeClose(() => confirm('Discard changes?'));
}
}| Token | Available in | Value |
|---|---|---|
DIALOG_REF |
Host + Content | DialogRef instance |
DIALOG_DATA |
Content | The dialogData object from config |
DIALOG_CONFIG |
Host | The full IDialogConfig |
HOST_DATA |
Host | The hostData object from config |
When you omit hostComponent from the config, a built-in DefaultDialogHostComponent is used. It provides a minimal wrapper with a close button. Good for simple use cases.
For a consistent dialog frame across your app (title bar, footer, custom styling), create your own host:
- Extend
NgxZeroDialogHost<T>whereTis the type of yourhostData - Import
DialogContentDirectiveand place<ng-template dialogContent></ng-template>in your template - Call
super()in the constructor
interface MyHostData {
title: string;
}
@Component({
standalone: true,
selector: 'app-dialog-host',
imports: [DialogContentDirective],
template: `
<div class="header">
<h2>{{ hostData.title }}</h2>
<button (click)="close()">×</button>
</div>
<div class="body">
<ng-template dialogContent></ng-template>
</div>
`,
})
export class AppDialogHostComponent extends NgxZeroDialogHost<MyHostData> {
constructor() {
super();
}
close() {
this.dialogRef.close();
}
}Use it:
this.dialog.openDialog(MyContentComponent, {
hostComponent: AppDialogHostComponent,
hostData: { title: 'Settings' },
}).subscribe();NgxZeroDialogHost automatically injects dialogRef, dialogConfig, and hostData for you. It also handles backdrop-click and Escape key behavior based on the config.
ngx-zero-dialog uses CSS class toggling for enter/leave animations. Two classes are applied to the <dialog> element:
ngx-zero-dialog-hidden— applied on creation (before open transition)ngx-zero-dialog-visible— added aftershowModal()(triggers open), removed on close (triggers close)
The built-in styles provide a simple opacity fade:
dialog.ngx-zero-dialog-hidden {
opacity: 0;
transition: opacity 0.2s ease-in-out;
&::backdrop {
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
}
dialog.ngx-zero-dialog-visible {
opacity: 1;
transition: opacity 0.2s ease-in-out;
&::backdrop {
opacity: 1;
background: rgba(0, 0, 0, 0.65);
backdrop-filter: blur(5px);
transition: opacity 0.2s ease-in-out;
}
}Override these in your global styles for custom animations (e.g. slide-in):
dialog.ngx-zero-dialog-hidden {
transform: translateY(100vh);
}
dialog.ngx-zero-dialog-visible {
transform: translateY(0);
}Disable animations globally via provideNgxZeroDialog({ ..., enableAnimations: false }) or per-dialog with animated: false in the config.
interface INgxZeroDialogConfig {
/** ID of the container element where dialogs are appended. Required. */
containerNodeID: string;
/** Globally enable/disable animations. Default: true */
enableAnimations?: boolean;
}MIT
Feel free to open discussions, raise issues, or submit pull requests on GitHub.