Building Reusable Modals in Angular with the CDK Overlay
Every Angular developer eventually hits the wall of creating modals. Out of the box, Angular doesn't give you a straightforward way to build a reusable modal — sure, third-party libraries exist, but they bring their own learning curves and can be tricky to integrate with your project's specific needs.
The Angular team, however, provides a solid solution through the Angular CDK (Component Dev Kit). With the CDK's Overlay module, you can create flexible, accessible modals without pulling in heavy external dependencies.
The Core Concept
The plan is straightforward: build a modal using the CDK's Overlay module, render the modal's content through an ng-template, and ensure the result is WCAG 2.0 compliant. This approach gives you full control over the modal's behavior and appearance while keeping the code clean and reusable.
Why the Overlay Module?
The OverlayModule from Angular CDK is one of the cleanest ways to add floating panels to your application. It's designed to handle everything from dropdown menus to full modals. Since the CDK is broken into small, focused packages, you only import what you actually use — no bloat, no unnecessary dependencies.
Setting Up
Start by installing the Angular CDK in your project. From the root directory of your Angular app, run:
npm install @angular/cdk
or
ng add @angular/cdk
Once installed, you need to import the OverlayModule in your app.module.ts and add it to the imports array.
import { OverlayModule } from '@angular/cdk/overlay';
....
imports: [
...
OverlayModule
]
OverlayModule Essentials
The heart of this approach is the create() method from the Overlay service. This method returns an OverlayRef, which acts as a PortalOutlet — the bridge between your overlay and the content it displays.
The create() method accepts a configuration object that controls how the overlay behaves and appears. The most frequently used configuration options include:
backdropClass: string [] //Adds the custom class for backdrop of modal.
hasBackdrop: boolean //Whether overlay will have backdrop or not.
height: number | string //defines the height of overlay.
width: number | string //defines the width of overlay.
pannelClass: string [] //Custom class for overlay.
positionStrategy: PositionStrategy //How will the overlay be placed on screen.
scrollStrategy: ScrollStrategy //how scrolling will happen on the overlay.
Here's an example of how you'd configure the overlay with these options:
const overlayRef = overlay.create({
height: '20vh',
width: '80vw',
.... // an example of creating an overlay
});
The overlayRef exposes an attach() method that accepts a portal instance. That portal carries the UI you want to display inside the overlay.
Using Portals to Render Content
Portals are a powerful concept in Angular CDK — they let you take content defined in one part of your component tree and render it somewhere else, dynamically. A portal can be a component, a TemplateRef, or a plain DOM element.
In this article, we'll use a TemplatePortal, which means the modal structure is defined inside an ng-template block in your component's template. This keeps the modal markup close to where it's used while still allowing it to be detached and rendered in the overlay.
<ng-template #template >
<div class="modal-card" >
<header class="modal-card-head">
<h5 tabindex="0" >{{title}}</h5>
<a tabindex="0" (click)="closeModal()" (keydown)="handleKey($event)" role="button" aria-label="Close Modal">
<span>X</span>
</a>
</header>
<section class="modal-card-body">
<p class="regular" tabindex="0">{{body}}</p>
</section>
</div>
</ng-template>
That's the basic modal skeleton: a modal-card wrapper containing a header section and a content section.
Next, we need to grab that template inside the TypeScript file. For this, we import TemplateRef, ViewChild, and ViewContainerRef.
@ViewChild('template') tpl! : TemplateRef<unknown>;
constructor(..., private viewContainerRef: ViewContainerRef);
With those imports in place, we can now attach the template to the overlay reference when the modal opens.
openWithTemplate() {
const config = new OverlayConfig({
hasBackdrop: true,
panelClass: ['modal', 'active'],
backdropClass: 'modal-backdrop',
scrollStrategy: this.overlay.scrollStrategies.block(),
});
this.overlayRef = this.overlay.create(config);
this.overlayRef.attach(new TemplatePortal(this.tpl, this.viewContainerRef));
}
The openWithTemplate() function you see above is all it takes to launch the modal — wire it to any button's click event and you're in business.
Closing the modal is just as simple. Looking at the modal's HTML, you'll notice a close() function is already wired up to the close anchor. The implementation is minimal:
closeModal(){
this.overlayRef.dispose();
}
This is also the reason we declared overlayRef as a global variable in the component — so both the open and close methods can access it easily.
Making It Accessible
Accessibility isn't optional anymore; it's a fundamental part of building web applications. A significant number of users rely on screen readers and other assistive technologies to navigate the web. The Web Content Accessibility Guidelines (WCAG) published by the W3C provide a framework for making content accessible to everyone.
Angular CDK includes the A11yModule, which gives developers the tools to meet those standards without reinventing the wheel.
A key WCAG requirement is that when a modal opens, focus should move inside it and stay trapped there until the modal closes. This means every interactive element inside the modal must remain reachable by keyboard navigation, and screen readers must announce them properly.
Some basic accessibility attributes were already present in the initial HTML we wrote. For example, the close anchor has role=button, which tells assistive technologies this is not a regular link but a button in disguise. The aria-label="Close" on the same element ensures screen readers announce it as "Close Button" rather than just reading the icon.
We also added tabindex attributes on several elements to guide keyboard navigation. But here's the catch: without focus trapping, a user can tab right out of the modal even while it's still open. That's exactly where the A11yModule steps in.
The cdkFocusInitial directive sets the initial focus point when the modal opens — in this case, the modal's title element gets focus first. The other two directives, cdkTrapFocus and cdkTrapFocusAutoCapture, handle focus management:
- cdkTrapFocusAutoCapture — moves focus into the trapped region when the trap initializes and returns focus to the previously focused element when the trap is destroyed.
- cdkTrapFocus — the main directive that activates the focus trap.
With these directives applied to the modal container, focus rotation stays confined inside the modal until it's closed. The updated HTML looks like this:
<ng-template #template >
<div class="modal-card" cdkTrapFocus cdkTrapFocusAutoCapture>
<header class="modal-card-head">
<h5 tabindex="0" cdkFocusInitial>{{title}}</h5>
<a tabindex="0" (click)="closeModal()" (keydown)="handleKey($event)" role="button" aria-label="Close Modal">
<span>X</span>
</a>
</header>
<section class="modal-card-body">
<p class="regular" tabindex="0">{{body}}</p>
</section>
</div>
</ng-template>
By adding these three directives plus the appropriate ARIA attributes, the modal now meets WCAG 2.0 standards.
Here's a quick look at the modal in action:
Wrapping Up
To take this further and make the modal truly reusable, extract the ng-template markup into its own standalone component. Once that's done, you'll have a modal you can drop anywhere in your application — no custom service required, no third-party library dependency.
This is a clean, lightweight way to add accessible modals to your Angular projects. The combination of CDK Overlay, TemplatePortal, and A11yModule gives you a solid foundation that fits right into any codebase.
If you have ideas on how to improve this pattern, I'd love to hear them. Let's keep the conversation going.
You can check out my portfolio at Find Sid and connect with me on LinkedIn. You can also see some of my art on Instagram — a follow is always appreciated.
