Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

How to create a modal in Angular

Building modal dialogs is essential for user interaction in modern web applications, whether for confirmations, forms, or displaying additional content. With over 10 years of experience building Angular applications since 2014 and as the creator of CoreUI, a widely used open-source UI library, I’ve implemented countless modals in production apps. The most reliable and modern approach is to use the Angular CDK Dialog, which provides a flexible, accessible foundation without imposing strict Material Design styling. This method gives you full control over appearance while handling focus trapping, keyboard navigation, and backdrop management automatically.

Use Angular CDK Dialog to create flexible, accessible modal dialogs with full control over styling and behavior.

import { Component, inject } from '@angular/core'
import { Dialog, DialogModule } from '@angular/cdk/dialog'

@Component({
  selector: 'app-modal-example',
  standalone: true,
  imports: [DialogModule],
  template: `
    <button (click)="openModal()">Open Modal</button>
  `
})
export class ModalExampleComponent {
  dialog = inject(Dialog)

  openModal() {
    this.dialog.open(ExampleModalComponent, {
      width: '500px',
      disableClose: false
    })
  }
}

This code imports the Dialog service from Angular CDK and injects it into the component. When the button is clicked, the openModal() method opens the modal by calling dialog.open() with the modal component and configuration options. The width property sets the modal size, while disableClose: false allows users to close the modal by clicking the backdrop or pressing Escape.

Creating the Modal Component

import { Component } from '@angular/core'
import { DialogRef } from '@angular/cdk/dialog'
import { CommonModule } from '@angular/common'

@Component({
  selector: 'app-example-modal',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div class="modal-overlay">
      <div class="modal-container">
        <div class="modal-header">
          <h2>Modal Title</h2>
          <button (click)="close()">×</button>
        </div>
        <div class="modal-body">
          <p>This is the modal content.</p>
        </div>
        <div class="modal-footer">
          <button (click)="close()">Cancel</button>
          <button (click)="confirm()">Confirm</button>
        </div>
      </div>
    </div>
  `,
  styles: [`
    .modal-overlay {
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background: rgba(0, 0, 0, 0.5);
      display: flex;
      align-items: center;
      justify-content: center;
    }
    .modal-container {
      background: white;
      border-radius: 8px;
      padding: 0;
      max-width: 90%;
      max-height: 90%;
      overflow: auto;
    }
    .modal-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 1rem;
      border-bottom: 1px solid #ddd;
    }
    .modal-body {
      padding: 1rem;
    }
    .modal-footer {
      padding: 1rem;
      border-top: 1px solid #ddd;
      display: flex;
      justify-content: flex-end;
      gap: 0.5rem;
    }
  `]
})
export class ExampleModalComponent {
  constructor(private dialogRef: DialogRef) {}

  close() {
    this.dialogRef.close()
  }

  confirm() {
    this.dialogRef.close('confirmed')
  }
}

The modal component defines the actual modal content and structure. It injects DialogRef to control the modal’s lifecycle. The close() method closes the modal without returning data, while confirm() closes it and returns a value that the opening component can use. The template includes a header with a close button, a body for content, and a footer with action buttons.

Passing Data to the Modal

import { Component, inject } from '@angular/core'
import { Dialog, DialogModule } from '@angular/cdk/dialog'

@Component({
  selector: 'app-data-modal-example',
  standalone: true,
  imports: [DialogModule],
  template: `
    <button (click)="openModalWithData()">Open Modal with Data</button>
  `
})
export class DataModalExampleComponent {
  dialog = inject(Dialog)

  openModalWithData() {
    const dialogRef = this.dialog.open(DataModalComponent, {
      width: '600px',
      data: {
        title: 'Delete Item',
        message: 'Are you sure you want to delete this item?',
        itemId: 123
      }
    })

    dialogRef.closed.subscribe(result => {
      if (result === 'confirmed') {
        console.log('User confirmed the action')
      }
    })
  }
}

This example shows how to pass data to the modal through the data property in the configuration object. The opening component can also subscribe to the closed observable to handle the result when the modal closes. This pattern is useful for confirmation dialogs, forms, or any scenario where the modal needs input data and should return a result.

Receiving Data in the Modal Component

import { Component, inject } from '@angular/core'
import { DialogRef, DIALOG_DATA } from '@angular/cdk/dialog'
import { CommonModule } from '@angular/common'

@Component({
  selector: 'app-data-modal',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div class="modal-overlay">
      <div class="modal-container">
        <div class="modal-header">
          <h2>{{ data.title }}</h2>
          <button (click)="close()">×</button>
        </div>
        <div class="modal-body">
          <p>{{ data.message }}</p>
          <p>Item ID: {{ data.itemId }}</p>
        </div>
        <div class="modal-footer">
          <button (click)="close()">Cancel</button>
          <button (click)="confirm()">Confirm</button>
        </div>
      </div>
    </div>
  `
})
export class DataModalComponent {
  data = inject(DIALOG_DATA)
  private dialogRef = inject(DialogRef)

  close() {
    this.dialogRef.close()
  }

  confirm() {
    this.dialogRef.close('confirmed')
  }
}

The modal component receives data by injecting the DIALOG_DATA token. This data is type-safe if you define an interface for it. The injected data can be used directly in the template or component logic, making it easy to create reusable modal components that adapt based on the provided data.

Configuring Backdrop and Keyboard Behavior

import { Component, inject } from '@angular/core'
import { Dialog, DialogModule } from '@angular/cdk/dialog'

@Component({
  selector: 'app-config-modal-example',
  standalone: true,
  imports: [DialogModule],
  template: `
    <button (click)="openConfiguredModal()">Open Configured Modal</button>
  `
})
export class ConfigModalExampleComponent {
  dialog = inject(Dialog)

  openConfiguredModal() {
    this.dialog.open(ExampleModalComponent, {
      width: '700px',
      height: '400px',
      disableClose: true,
      hasBackdrop: true,
      backdropClass: 'custom-backdrop',
      panelClass: 'custom-modal-panel',
      closeOnNavigation: true,
      closeOnDestroy: true
    })
  }
}

This configuration example shows the various options available for customizing modal behavior. Setting disableClose: true prevents closing the modal by clicking the backdrop or pressing Escape, forcing users to use the modal’s buttons. The hasBackdrop option controls whether a backdrop is displayed, backdropClass adds custom CSS classes to the backdrop, and panelClass adds classes to the modal panel. The closeOnNavigation option automatically closes the modal when the route changes, while closeOnDestroy ensures cleanup when the component is destroyed.

Creating a Reusable Modal Service

import { Injectable, inject } from '@angular/core'
import { Dialog } from '@angular/cdk/dialog'
import { Observable } from 'rxjs'

interface ConfirmDialogData {
  title: string
  message: string
  confirmText?: string
  cancelText?: string
}

@Injectable({
  providedIn: 'root'
})
export class ModalService {
  private dialog = inject(Dialog)

  confirm(data: ConfirmDialogData): Observable<any> {
    const dialogRef = this.dialog.open(ConfirmModalComponent, {
      width: '500px',
      data: {
        title: data.title,
        message: data.message,
        confirmText: data.confirmText || 'Confirm',
        cancelText: data.cancelText || 'Cancel'
      }
    })

    return dialogRef.closed
  }

  openCustom(component: any, config: any = {}) {
    return this.dialog.open(component, {
      width: '600px',
      ...config
    })
  }
}

Creating a modal service centralizes modal logic and makes it easier to open modals from anywhere in your application. The service provides a confirm() method for confirmation dialogs and an openCustom() method for opening any modal component. This pattern simplifies the code in your components and ensures consistent modal behavior throughout the application.

Handling Modal Result with Async/Await

import { Component, inject } from '@angular/core'
import { ModalService } from './modal.service'

@Component({
  selector: 'app-async-modal-example',
  standalone: true,
  template: `
    <button (click)="deleteItem()">Delete Item</button>
  `
})
export class AsyncModalExampleComponent {
  modalService = inject(ModalService)

  async deleteItem() {
    const result = await this.modalService.confirm({
      title: 'Confirm Deletion',
      message: 'Are you sure you want to delete this item?',
      confirmText: 'Delete',
      cancelText: 'Cancel'
    }).toPromise()

    if (result === 'confirmed') {
      console.log('Item deleted')
      // Perform deletion logic here
    } else {
      console.log('Deletion cancelled')
    }
  }
}

This example demonstrates using async/await with modal dialogs by converting the observable to a promise with toPromise(). This approach makes the code more readable and easier to follow, especially when you need to perform actions based on the modal result. The method waits for the user to interact with the modal before continuing execution.

Best Practice Note

For production applications, consider using pre-built modal components from CoreUI for Angular, which provide consistent styling, accessibility features, and additional functionality out of the box. You can explore the CoreUI Angular Modal component for a production-ready solution that follows best practices and includes features like stacking, scrollable content, and responsive behavior. This is the same pattern we use in CoreUI components to ensure reliable and accessible modal dialogs. For more advanced overlay patterns, check out how to create a custom overlay in Angular and how to create a tooltip in Angular.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author