A reusable Angular modal component should separate the dialog shell from the content placed inside it. The shell owns visibility, backdrop behavior, size, and close events; the parent supplies the header, body, and footer. This keeps page-specific forms and actions out of the shared component.

This article preserves the Angular 18.2 implementation and demonstration published on December 4, 2024, while explaining its real production boundary. The historical code worked for the original project, but it has not been rebuilt or tested against the current Angular release. The accessibility gaps documented below must be addressed before treating it as a production-ready dialog.

Define the modal component boundary

The component exposes three inputs and one output. visible controls whether the modal exists in the DOM, size selects a width class, and clickBackdropToClose decides whether a click outside the dialog closes it. The closed event lets the parent reset its own state.

  • The modal owns: the backdrop, dialog frame, size class, animation, and close signal.
  • The parent owns: the business data, form state, validation, save operation, and the decision to open the modal.
  • Projected content owns: the visible heading, body controls, and action buttons for one use case.

This boundary is useful when several screens need the same visual shell but not the same form. It also avoids a shared component that knows about courses, employees, or any other domain object.

Build the Angular modal component class

The original repository uses Angular 18.2 and the legacy @angular/animations trigger API. The following class is preserved from that working demonstration.

import { Component, Input, Output, EventEmitter } from '@angular/core';
import { trigger, style, transition, animate } from '@angular/animations';

@Component({
  selector: 'dnc-modal',
  templateUrl: './dnc-modal.component.html',
  styleUrls: ['./dnc-modal.component.css'],
  animations: [
    trigger('fade', [
      transition(':enter', [
        style({ opacity: 0 }),
        animate('300ms ease-in', style({ opacity: 1 })),
      ]),
      transition(':leave', [
        animate('300ms ease-out', style({ opacity: 0 })),
      ]),
    ]),
  ],
})
export class DncModalComponent {
  @Input() visible = false;
  @Input() size: 'small' | 'normal' | 'large' = 'normal';
  @Input() clickBackdropToClose = true;
  @Output() closed = new EventEmitter<void>();

  show(): void {
    this.visible = true;
  }

  hide(): void {
    this.visible = false;
    this.closed.emit();
  }

  onBackdropClick(event: MouseEvent): void {
    if (this.clickBackdropToClose && event.target === event.currentTarget) {
      this.hide();
    }
  }

  getModalSizeClass(): string {
    switch (this.size) {
      case 'small': return 'dnc-modal-sm';
      case 'large': return 'dnc-modal-lg';
      default: return 'dnc-modal-md';
    }
  }
}

The equality check between event.target and event.currentTarget matters. A click on the backdrop has the same target and current target, while a click on a child inside the dialog does not. Without that guard, interacting with a form control could close the modal through event bubbling.

The parent should still be the source of truth for visibility. Calling hide() updates the child immediately and emits closed, but the parent must also set its bound state to false. In a new implementation, a two-way model API or an explicit visibleChange event can make that contract clearer.

Project the header, body, and footer

Angular content projection keeps the modal generic. Three <ng-content> placeholders select elements marked for the header, body, and footer. The parent can then supply any appropriate markup without adding domain inputs to the modal class.

<div
  *ngIf="visible"
  class="dnc-modal-backdrop"
  @fade
  (click)="onBackdropClick($event)">
  <div
    class="dnc-modal-dialog"
    [ngClass]="getModalSizeClass()"
    role="dialog"
    aria-modal="true">
    <div class="dnc-modal-content">
      <div class="dnc-modal-header">
        <ng-content select="[modal-header]"></ng-content>
        <button
          type="button"
          class="btn-close"
          aria-label="Close"
          (click)="hide()"></button>
      </div>

      <div class="dnc-modal-body">
        <ng-content select="[modal-body]"></ng-content>
      </div>

      <div class="dnc-modal-footer">
        <ng-content select="[modal-footer]"></ng-content>
      </div>
    </div>
  </div>
</div>

The selectors match attributes on projected elements; they are not extra Angular components. Content projection is resolved from the component template, so do not conditionally add or remove the <ng-content> placeholders themselves. Control whether the whole modal is rendered instead.

Style the backdrop and modal sizes

The backdrop fills the viewport and centers the dialog. The three size classes cap the dialog width, while the content panel supplies the background, border radius, and shadow.

.dnc-modal-backdrop {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: rgba(0, 0, 0, 0.6);
  z-index: 1050;
}

.dnc-modal-dialog {
  max-width: 100%;
  margin: 0;
}

.dnc-modal-sm { max-width: 300px; }
.dnc-modal-md { max-width: 500px; }
.dnc-modal-lg { max-width: 800px; }

.dnc-modal-content {
  background: #fff;
  border-radius: 0.5rem;
  overflow: hidden;
  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
}

.dnc-modal-header,
.dnc-modal-footer {
  display: flex;
  align-items: center;
  padding: 1rem 1.5rem;
  background-color: #f8f9fa;
}

.dnc-modal-header {
  justify-content: space-between;
  border-bottom: 1px solid #dee2e6;
}

.dnc-modal-body {
  padding: 1.5rem;
}

.dnc-modal-footer {
  justify-content: flex-end;
  border-top: 1px solid #dee2e6;
}

The historical stylesheet needs extra constraints for narrow or short viewports. A production version should add a viewport-relative width, a maximum height, internal scrolling, and safe spacing from the screen edge. It should also prevent the page behind the modal from scrolling while the dialog is open.

.dnc-modal-dialog {
  width: min(90vw, 500px);
  max-height: 90vh;
}

.dnc-modal-content {
  max-height: 90vh;
  display: flex;
  flex-direction: column;
}

.dnc-modal-body {
  overflow-y: auto;
}

This addition is guidance, not a claim that the original video or repository included it. Choose one width system rather than combining conflicting fixed and fluid values.

Use the modal from a parent component

The parent owns the open flag and handles the close notification. The same modal shell can then host different projected content without duplicating the backdrop and layout.

<button type="button" (click)="courseModalOpen = true">
  Add course
</button>

<dnc-modal
  [visible]="courseModalOpen"
  size="normal"
  [clickBackdropToClose]="false"
  (closed)="courseModalOpen = false">
  <h2 modal-header id="course-modal-title">Add course</h2>

  <form modal-body id="course-form" (ngSubmit)="saveCourse()">
    <label for="course-name">Course name</label>
    <input id="course-name" name="courseName" [(ngModel)]="courseName" />
  </form>

  <div modal-footer>
    <button type="button" (click)="courseModalOpen = false">
      Cancel
    </button>
    <button type="submit" form="course-form">
      Save
    </button>
  </div>
</dnc-modal>

Disabling backdrop close is safer when the dialog contains unsaved form data. The example also associates the Save button with the projected form through its form attribute, so the footer can remain outside the form element.

If the form needs an autocomplete field, the Angular autocomplete component guide explains a reusable form-control boundary. For a server-rendered alternative, compare the lifecycle and focus requirements with the reusable Blazor modal component.

Fix the accessibility limits before production

role="dialog" and aria-modal="true" do not make a modal accessible by themselves. The original component omits several behaviors required by the WAI-ARIA modal dialog pattern.

  • Move focus inside on open. Put initial focus on the heading, first meaningful control, or another deliberate target.
  • Trap keyboard focus. Tab and Shift+Tab must cycle within the open dialog.
  • Support Escape. Escape should close the dialog unless the workflow has a documented reason to block it.
  • Restore focus. After close, return focus to the control that opened the dialog or to another logical element.
  • Give the dialog an accessible name. Connect the dialog to a visible heading with aria-labelledby, or provide an accurate aria-label.
  • Make the background inert. Users must not interact with content outside a modal dialog while it is open.

The projected heading in the parent example has id="course-modal-title", but the historical modal template does not expose an input that assigns that value to aria-labelledby. Add that contract before shipping. A robust implementation should also handle nested overlays, multiple modal requests, scroll locking, and cleanup when the component is destroyed.

Do not mark a non-modal popover with aria-modal="true". That attribute tells assistive technology that the rest of the page is unavailable, so the application behavior must match the promise.

Plan a migration to current Angular

The repository records Angular 18.2. As of September 2, 2026, Angular 18 is outside the supported release window, while newer Angular lines are active or in long-term support. That does not erase the historical result, but it changes how the sample should be used.

  • Start from the repository when you need to understand the original design, not as proof of compatibility with a current workspace.
  • Run Angular’s supported update path one major version at a time and review each migration.
  • Replace obsolete animation patterns according to the current Angular animation guidance rather than mechanically copying the Angular 18 setup.
  • Retest projection, focus behavior, backdrop clicks, keyboard interaction, forms, and viewport constraints after migration.
  • Run the test in the same rendering mode used by the real application. Server-side rendering and hydration behavior were not tested for this refresh.

A new application may also be better served by the browser’s native <dialog> element or an established Angular overlay library. The custom component remains useful when its small API is intentional and the team is prepared to own focus management, stacking, scrolling, and assistive-technology testing.

Verify the historical implementation

The repository is the evidence for the original Angular 18.2 implementation. To reproduce it, use a Node.js version compatible with that Angular workspace, install the locked dependencies, and start the development server. The commands below are verification instructions; they were not rerun as part of this editorial refresh.

git clone https://github.com/dotnetcodercom/reusable-angular-modal-component.git
cd reusable-angular-modal-component/dnc-modal
npm ci
npm start
  • Open and close each modal from the control that launches it.
  • Verify small, normal, and large widths at desktop and narrow viewport sizes.
  • Confirm that a backdrop click closes only when clickBackdropToClose is enabled.
  • Confirm that clicks inside the dialog never trigger the backdrop handler.
  • Check that the closed event resets parent state.
  • Verify projected header, body, and footer content for each example.
  • Test Tab, Shift+Tab, Escape, initial focus, and restored focus. Expect the historical component to expose the accessibility gaps listed above until they are implemented.
  • Inspect the browser console and run the workspace tests and production build before using migrated code.

Watch the original video and inspect the source

The video below is the original demonstration recorded for the 2024 article. It shows the historical result and workflow; it is not evidence that the repository was retested against a later Angular release.

The practical design is still sound: keep reusable shell behavior in one component and keep business state in the parent. The production work lies in the details—especially focus, keyboard handling, scroll control, responsive sizing, and a deliberate migration from Angular 18.

References

Historical demo: review the original Angular 18.2 source code on GitHub.

Found this useful? Support more practical developer content.

Author

Practical .NET, Angular, Azure, Blazor, and AI engineering for real-world development.

Ads Blocker Image Powered by Code Help Pro

Ads Blocker Detected!!!

We have detected that you are using extensions to block ads. Please support us by disabling these ads blocker.

Powered By
Best Wordpress Adblock Detecting Plugin | CHP Adblock