How to create a custom overlay in Angular
Creating overlays like tooltips, popovers, and custom dropdowns requires precise positioning and z-index management that can quickly become complex. With 12 years of experience building Angular applications since 2014 and as the creator of CoreUI, I’ve built countless overlay components for enterprise applications. The most reliable solution is Angular CDK’s Overlay service, which provides flexible positioning, scroll strategies, and backdrop handling out of the box. This approach eliminates positioning bugs and works seamlessly across different screen sizes and scroll containers.
Use Angular CDK Overlay service with a positioning strategy to create flexible, reusable overlay components.
import { Overlay, OverlayRef } from '@angular/cdk/overlay'
import { ComponentPortal } from '@angular/cdk/portal'
import { Component, Injectable } from '@angular/core'
@Injectable({
providedIn: 'root'
})
export class OverlayService {
constructor(private overlay: Overlay) {}
openOverlay(origin: HTMLElement, content: any): OverlayRef {
const positionStrategy = this.overlay
.position()
.flexibleConnectedTo(origin)
.withPositions([{
originX: 'center',
originY: 'bottom',
overlayX: 'center',
overlayY: 'top'
}])
const overlayRef = this.overlay.create({
positionStrategy,
hasBackdrop: true,
backdropClass: 'cdk-overlay-transparent-backdrop'
})
const portal = new ComponentPortal(content)
overlayRef.attach(portal)
return overlayRef
}
}
This service creates an overlay positioned below and centered on the origin element. The flexibleConnectedTo() method attaches the overlay to the origin, and withPositions() defines fallback positions if the primary position doesn’t fit on screen. The backdrop allows users to close the overlay by clicking outside, and the overlay reference provides control for dismissing the overlay programmatically.
Installing Angular CDK
Before using the Overlay service, you need to install Angular CDK and import the necessary modules.
npm install @angular/cdk
import { OverlayModule } from '@angular/cdk/overlay'
import { PortalModule } from '@angular/cdk/portal'
import { NgModule } from '@angular/core'
@NgModule({
imports: [
OverlayModule,
PortalModule
]
})
export class AppModule {}
The OverlayModule provides the Overlay service and directives, while PortalModule enables attaching components or templates to overlay containers. Import both modules in your root module or the feature module where you’ll use overlays.
Creating a Custom Overlay Component
To display content in the overlay, create a component that will be rendered inside the overlay container.
import { Component } from '@angular/core'
@Component({
selector: 'app-custom-overlay',
template: `
<div class='overlay-content'>
<h3>Custom Overlay</h3>
<p>This content appears in an overlay positioned relative to the trigger element.</p>
<button (click)='onClose()'>Close</button>
</div>
`,
styles: [`
.overlay-content {
background: white;
border: 1px solid #ccc;
border-radius: 4px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
max-width: 300px;
}
`]
})
export class CustomOverlayComponent {
onClose() {
// Close logic will be injected
}
}
This component defines the overlay’s visual content and styling. The close button will be connected to the overlay reference to dismiss the overlay when clicked. You can inject data or callbacks into this component to make it dynamic and reusable.
Using the Overlay in a Component
Create a trigger component that opens the overlay when a user interacts with an element.
import { Component, ViewChild, ElementRef } from '@angular/core'
import { OverlayRef } from '@angular/cdk/overlay'
import { OverlayService } from './overlay.service'
import { CustomOverlayComponent } from './custom-overlay.component'
@Component({
selector: 'app-overlay-demo',
template: `
<button #trigger (click)='openOverlay()'>
Open Overlay
</button>
`
})
export class OverlayDemoComponent {
@ViewChild('trigger', { read: ElementRef }) trigger!: ElementRef
private overlayRef?: OverlayRef
constructor(private overlayService: OverlayService) {}
openOverlay() {
if (this.overlayRef) {
return
}
this.overlayRef = this.overlayService.openOverlay(
this.trigger.nativeElement,
CustomOverlayComponent
)
this.overlayRef.backdropClick().subscribe(() => {
this.closeOverlay()
})
}
closeOverlay() {
this.overlayRef?.dispose()
this.overlayRef = undefined
}
}
This component uses ViewChild to get a reference to the trigger button element. When clicked, it calls the overlay service to create and position the overlay relative to the button. The backdropClick() observable allows closing the overlay when users click outside. The dispose() method removes the overlay from the DOM and cleans up resources.
Advanced Positioning Strategies
Angular CDK supports multiple positioning strategies with automatic fallback positions.
import { Overlay } from '@angular/cdk/overlay'
import { Injectable } from '@angular/core'
@Injectable({
providedIn: 'root'
})
export class AdvancedOverlayService {
constructor(private overlay: Overlay) {}
openWithFallbackPositions(origin: HTMLElement, content: any) {
const positionStrategy = this.overlay
.position()
.flexibleConnectedTo(origin)
.withPositions([
{ originX: 'center', originY: 'bottom', overlayX: 'center', overlayY: 'top', offsetY: 8 },
{ originX: 'center', originY: 'top', overlayX: 'center', overlayY: 'bottom', offsetY: -8 },
{ originX: 'end', originY: 'center', overlayX: 'start', overlayY: 'center', offsetX: 8 }
])
.withPush(false)
return this.overlay.create({
positionStrategy,
hasBackdrop: true
})
}
}
This service defines fallback positions: below, above, and right. The overlay tries each position in order until it finds one that fits within the viewport. The offsetY and offsetX properties add spacing between the origin and overlay.
Implementing Custom Backdrops
Backdrops provide visual separation and allow users to dismiss overlays by clicking outside the content area.
import { Overlay, OverlayRef } from '@angular/cdk/overlay'
import { ComponentPortal } from '@angular/cdk/portal'
import { Injectable } from '@angular/core'
@Injectable({
providedIn: 'root'
})
export class BackdropOverlayService {
constructor(private overlay: Overlay) {}
openWithBackdrop(origin: HTMLElement, content: any): OverlayRef {
const positionStrategy = this.overlay
.position()
.flexibleConnectedTo(origin)
.withPositions([{
originX: 'center',
originY: 'bottom',
overlayX: 'center',
overlayY: 'top'
}])
const overlayRef = this.overlay.create({
positionStrategy,
hasBackdrop: true,
backdropClass: 'custom-overlay-backdrop',
scrollStrategy: this.overlay.scrollStrategies.block()
})
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose()
})
const portal = new ComponentPortal(content)
overlayRef.attach(portal)
return overlayRef
}
}
/* global styles */
.custom-overlay-backdrop {
background-color: rgba(0, 0, 0, 0.5);
}
This service creates a semi-transparent backdrop that blocks scrolling when the overlay is open. The backdropClick() subscription automatically closes the overlay when users click the backdrop. The scroll strategy prevents the page from scrolling while the overlay is active, keeping focus on the overlay content.
Managing Scroll Strategies
Scroll strategies control how overlays behave when the page or origin element scrolls.
import { Overlay, OverlayRef } from '@angular/cdk/overlay'
import { Injectable } from '@angular/core'
@Injectable({
providedIn: 'root'
})
export class ScrollStrategyService {
constructor(private overlay: Overlay) {}
openWithReposition(origin: HTMLElement, content: any): OverlayRef {
const scrollStrategy = this.overlay.scrollStrategies.reposition()
const positionStrategy = this.overlay
.position()
.flexibleConnectedTo(origin)
.withPositions([{
originX: 'center',
originY: 'bottom',
overlayX: 'center',
overlayY: 'top'
}])
return this.overlay.create({
positionStrategy,
scrollStrategy,
hasBackdrop: true
})
}
}
The reposition() strategy keeps the overlay attached to the origin element as it moves during scrolling, ideal for tooltips and popovers. Other strategies include close() which dismisses the overlay when scrolling starts, block() which prevents scrolling entirely, and noop() which does nothing. Choose the strategy that matches your overlay’s behavior requirements.
Passing Data to Overlay Components
You can pass data to overlay components using CDK Portal’s injector system.
import { Overlay, OverlayRef } from '@angular/cdk/overlay'
import { ComponentPortal } from '@angular/cdk/portal'
import { Injectable, Injector, InjectionToken } from '@angular/core'
export const OVERLAY_DATA = new InjectionToken<any>('OVERLAY_DATA')
@Injectable({
providedIn: 'root'
})
export class DataOverlayService {
constructor(
private overlay: Overlay,
private injector: Injector
) {}
openWithData(origin: HTMLElement, content: any, data: any): OverlayRef {
const overlayRef = this.overlay.create({
positionStrategy: this.overlay
.position()
.flexibleConnectedTo(origin)
.withPositions([{
originX: 'center',
originY: 'bottom',
overlayX: 'center',
overlayY: 'top'
}]),
hasBackdrop: true
})
const injector = Injector.create({
parent: this.injector,
providers: [
{ provide: OVERLAY_DATA, useValue: data },
{ provide: OverlayRef, useValue: overlayRef }
]
})
const portal = new ComponentPortal(content, null, injector)
overlayRef.attach(portal)
return overlayRef
}
}
This creates a custom injector that provides both the overlay data and the overlay reference to the component. The component can inject these values using @Inject(OVERLAY_DATA) and OverlayRef to access the data and control the overlay.
Best Practice Note
This is the same approach we use in CoreUI Angular components to create modals, tooltips, and popovers with precise positioning and scroll handling. For simpler modal dialogs, check out how to create a modal in Angular, and for smaller contextual overlays, see how to create a tooltip in Angular. Always use CDK Overlay instead of custom absolute positioning, as it handles edge cases like viewport boundaries, scroll containers, and RTL layouts automatically. When building enterprise applications with CoreUI for Angular, leverage the built-in overlay components for consistent behavior across your application.



