Skip to main content

Dialog

The Dialog component is a fully headless and accessible UI primitive for creating modal dialogs and overlays. It's inspired by HeadlessUI Dialog, but built natively for Angular.

It is useful for building modal windows, confirmation dialogs, forms, and other overlay content — while giving you complete control over markup and styling.

When to use it

Use Dialog when you need a modal overlay component that is:

  • Fully customizable in layout and appearance
  • Accessible with screen readers and keyboard navigation
  • Modal with backdrop and focus management
  • Dismissible via escape key
  • Compatible with complex content layouts

Common use cases:

  • Confirmation dialogs
  • Modal forms
  • Image lightboxes
  • Alert dialogs
  • Settings panels

Anatomy

A complete Dialog is composed of four components:

  • Dialog - The provider and wrapper. Manages internal open/close state and modal behavior.
  • DialogPanel - The main content container for the dialog.
  • DialogTitle - The title/heading for the dialog (for accessibility).
  • DialogDescription - The description text for the dialog (for accessibility).

All four components are standalone and composable.

Features

  • ✅ Accessible by default (ARIA attributes)
  • ✅ Tailwind-friendly (no styles imposed)
  • ✅ Multiple selectors supported:
    • <Dialog>
    • <div ngxDialog>
    • <ngx-headlessui-dialog>
  • ✅ Works with both #templateRefs and Angular DI (DialogContextService)
  • ✅ Supports multiple instances per page
  • ✅ Escape key to close
  • ✅ Modal behavior with aria-modal

Installation

Dialog ships as part of the @ngx-headless/ui by default. Install if you haven't already.

npm install @ngx-headless/ui

Import the components directly:

import {
DialogComponent,
DialogPanelComponent,
DialogTitleComponent,
DialogDescriptionComponent,
} from "@ngx-headless/ui";

Usage Examples

Template Reference — Basic Dialog

<Dialog #d="ngxDialog">
<div *ngIf="d.isOpen()" class="fixed inset-0 bg-black bg-opacity-50">
<DialogPanel class="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white p-6 rounded-lg">
<DialogTitle class="text-lg font-bold">Dialog Title</DialogTitle>
<DialogDescription class="mt-2 text-gray-600">
This is the dialog content.
</DialogDescription>
<button (click)="d.close()" class="mt-4 px-4 py-2 bg-blue-500 text-white rounded">
Close
</button>
</DialogPanel>
</div>
</Dialog>

Template Reference — Confirmation Dialog

<Dialog #confirmDialog="ngxDialog">
<div *ngIf="confirmDialog.isOpen()" class="fixed inset-0 z-50">
<!-- Backdrop -->
<div class="fixed inset-0 bg-black bg-opacity-50"></div>

<!-- Dialog -->
<div class="fixed inset-0 flex items-center justify-center p-4">
<DialogPanel class="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
<DialogTitle class="text-lg font-semibold text-gray-900">
Confirm Action
</DialogTitle>
<DialogDescription class="mt-2 text-sm text-gray-500">
Are you sure you want to proceed? This action cannot be undone.
</DialogDescription>
<div class="mt-6 flex gap-3 justify-end">
<button (click)="confirmDialog.close()"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200">
Cancel
</button>
<button (click)="handleConfirm(); confirmDialog.close()"
class="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-md hover:bg-red-700">
Confirm
</button>
</div>
</DialogPanel>
</div>
</div>
</Dialog>

Accessibility

This component handles

  • role="dialog" on the Dialog
  • aria-modal="true" when the dialog is open
  • aria-labelledby pointing to the DialogTitle
  • aria-describedby pointing to the DialogDescription
  • Keyboard interaction: Escape key to close
  • Screen reader compatibility for modal state

Animations

DialogPanel and backdrop elements can be animated freely using Angular's built-in animation system.

Example

Add animation triggers to your component:

import { trigger, transition, style, animate } from "@angular/animations";

@Component({
animations: [
trigger("fadeIn", [
transition(":enter", [
style({ opacity: 0 }),
animate("150ms ease-out", style({ opacity: 1 })),
]),
transition(":leave", [
animate("100ms ease-in", style({ opacity: 0 })),
]),
]),
trigger("slideUp", [
transition(":enter", [
style({ opacity: 0, transform: "translateY(4px) scale(0.95)" }),
animate("150ms ease-out", style({ opacity: 1, transform: "translateY(0) scale(1)" })),
]),
transition(":leave", [
animate("100ms ease-in", style({ opacity: 0, transform: "translateY(4px) scale(0.95)" })),
]),
]),
],
})
export class ExampleComponent {}

Use them in the template:

<Dialog #dialog="ngxDialog">
<div *ngIf="dialog.isOpen()" @fadeIn class="fixed inset-0 bg-black bg-opacity-50"></div>
<DialogPanel *ngIf="dialog.isOpen()" @slideUp>
Content goes here.
</DialogPanel>
</Dialog>

Component API

DialogComponent

InputTypeDescription
classstringClass for styling wrapper
MethodDescription
isOpen()Returns true if dialog open
toggle()Toggles dialog open/close
open()Opens dialog
close()Closes dialog

DialogPanelComponent

  • Visible only when Dialog is open
  • Can be styled or animated freely
  • hidden attribute used for visibility
  • Supports complex content layouts

DialogTitleComponent

  • Automatically sets id="dialog-title" for accessibility
  • Should be referenced by aria-labelledby on the dialog

DialogDescriptionComponent

  • Automatically sets id="dialog-description" for accessibility
  • Should be referenced by aria-describedby on the dialog

Focus Management

The Dialog component does not include built-in focus trapping. For production use, consider adding focus management:

Manual Focus Management

export class MyDialogComponent {
@ViewChild('firstFocusable') firstFocusable!: ElementRef;
@ViewChild('dialog') dialog!: DialogComponent;

openDialog() {
this.dialog.open();
// Focus first element when opened
setTimeout(() => this.firstFocusable.nativeElement.focus(), 0);
}

@HostListener('keydown.tab', ['$event'])
handleTab(event: KeyboardEvent) {
// Implement focus trapping logic here
}
}

With CDK a11y

import { FocusTrap, FocusTrapFactory } from '@angular/cdk/a11y';

export class MyDialogComponent implements OnDestroy {
private focusTrap?: FocusTrap;

constructor(private focusTrapFactory: FocusTrapFactory) {}

openDialog() {
this.dialog.open();
this.focusTrap = this.focusTrapFactory.create(this.dialogElement);
this.focusTrap.focusInitialElement();
}

closeDialog() {
this.dialog.close();
this.focusTrap?.destroy();
}
}

See Also