Angular CDK's Overlay module provides the low-level building blocks for creating popups—tooltips, popovers, dropdowns, and custom dialogs. Compared with manipulating the DOM directly or pulling in a third-party library, CDK Overlay integrates tightly with Angular's dependency injection and change detection, making it an ideal foundation for a component library.
Installation
npm install @angular/cdk
Core Concepts
Overlay(入口服务)
└── OverlayRef(单个弹出层的引用)
├── PositionStrategy(定位策略)
│ ├── GlobalPositionStrategy(全局定位,如居中 Modal)
│ └── ConnectedPositionStrategy(相对锚点定位,如 Dropdown)
└── ScrollStrategy(滚动行为)
├── CloseScrollStrategy(滚动时关闭)
├── BlockScrollStrategy(锁定滚动)
└── RepositionScrollStrategy(滚动时重新定位)
Basic Usage: Custom Tooltip
import { Overlay, OverlayRef } from "@angular/cdk/overlay";
import { TemplatePortal } from "@angular/cdk/portal";
@Directive({ selector: "[appTooltip]" })
export class TooltipDirective implements OnDestroy {
@Input("appTooltip") tooltipText = "";
@ViewChild("tooltipTemplate") tooltipTemplate!: TemplateRef<any>;
private overlayRef: OverlayRef | null = null;
constructor(
private overlay: Overlay,
private elementRef: ElementRef,
private viewContainerRef: ViewContainerRef,
) {}
@HostListener("mouseenter")
show() {
if (this.overlayRef) return;
// 创建定位策略:相对于宿主元素,显示在上方
const positionStrategy = this.overlay
.position()
.flexibleConnectedTo(this.elementRef)
.withPositions([
{
originX: "center",
originY: "top",
overlayX: "center",
overlayY: "bottom",
offsetY: -8,
},
{
originX: "center",
originY: "bottom",
overlayX: "center",
overlayY: "top",
offsetY: 8,
},
]);
this.overlayRef = this.overlay.create({
positionStrategy,
scrollStrategy: this.overlay.scrollStrategies.reposition(),
hasBackdrop: false,
});
const portal = new TemplatePortal(
this.tooltipTemplate,
this.viewContainerRef,
);
this.overlayRef.attach(portal);
}
@HostListener("mouseleave")
hide() {
this.overlayRef?.dispose();
this.overlayRef = null;
}
ngOnDestroy() {
this.overlayRef?.dispose();
}
}
<!-- tooltip.directive.ts 关联的模板 -->
<ng-template #tooltipTemplate>
<div class="tooltip">{{ tooltipText }}</div>
</ng-template>
<!-- 使用 -->
<button [appTooltip]="'这是提示信息'">悬停我</button>
Custom Dropdown Component
@Component({
selector: "app-dropdown",
template: `
<button #trigger (click)="toggle()">
{{ selectedLabel || placeholder }}
<mat-icon>arrow_drop_down</mat-icon>
</button>
<ng-template #dropdownPanel>
<div class="dropdown-panel" [@dropdownAnimation]>
<div
*ngFor="let option of options"
class="dropdown-option"
[class.selected]="option.value === value"
(click)="select(option)"
>
{{ option.label }}
</div>
</div>
</ng-template>
`,
})
export class DropdownComponent implements OnDestroy {
@Input() options: { label: string; value: any }[] = [];
@Input() placeholder = "请选择";
@Output() valueChange = new EventEmitter<any>();
@ViewChild("trigger") trigger!: ElementRef;
@ViewChild("dropdownPanel") panelTemplate!: TemplateRef<any>;
value: any = null;
private overlayRef: OverlayRef | null = null;
get selectedLabel() {
return this.options.find((o) => o.value === this.value)?.label;
}
constructor(
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
) {}
toggle() {
this.overlayRef ? this.close() : this.open();
}
open() {
const positionStrategy = this.overlay
.position()
.flexibleConnectedTo(this.trigger)
.withPositions([
{
originX: "start",
originY: "bottom",
overlayX: "start",
overlayY: "top",
},
]);
this.overlayRef = this.overlay.create({
positionStrategy,
scrollStrategy: this.overlay.scrollStrategies.reposition(),
hasBackdrop: true,
backdropClass: "cdk-overlay-transparent-backdrop",
minWidth: this.trigger.nativeElement.offsetWidth,
});
// 点击背景关闭
this.overlayRef.backdropClick().subscribe(() => this.close());
const portal = new TemplatePortal(
this.panelTemplate,
this.viewContainerRef,
);
this.overlayRef.attach(portal);
}
close() {
this.overlayRef?.dispose();
this.overlayRef = null;
}
select(option: { label: string; value: any }) {
this.value = option.value;
this.valueChange.emit(this.value);
this.close();
}
ngOnDestroy() {
this.overlayRef?.dispose();
}
}
Globally Centered Modal
openModal() {
const positionStrategy = this.overlay.position().global()
.centerHorizontally()
.centerVertically();
const overlayRef = this.overlay.create({
positionStrategy,
hasBackdrop: true,
backdropClass: 'dark-backdrop',
scrollStrategy: this.overlay.scrollStrategies.block() // 锁定背景滚动
});
const portal = new ComponentPortal(MyModalComponent);
const ref = overlayRef.attach(portal);
// 传递数据给 Modal 组件
ref.instance.data = { title: '确认操作' };
ref.instance.close.subscribe(() => overlayRef.dispose());
overlayRef.backdropClick().subscribe(() => overlayRef.dispose());
}
Summary
Angular CDK Overlay is a Swiss Army knife for building all kinds of popups. Once you master ConnectedPositionStrategy and backdropClick, you can build every common popup interaction—tooltips, dropdowns, popovers, and more. Compared with using the concrete components of a third-party UI library, CDK gives you far more room to customize while staying fully compatible with the Angular ecosystem.
