Angular CDK
The Angular Component Dev Kit (CDK) is a collection of unstyled, behavior-only primitives that power Angular Material — but you can use them on their own to build your own components. Instead of shipping finished widgets, the CDK gives you the hard parts: positioning floating panels, managing focus, virtual scrolling, drag-and-drop, and ARIA wiring. Because it carries no visual opinion, you get robust, accessible behavior while keeping full control over markup and styling. This makes it the foundation for any serious in-house component library.
Installing the CDK
The CDK ships as a standalone package and does not require Angular Material. Add it with the CLI so the correct version is resolved against your Angular release.
ng add @angular/cdk
Each feature lives in its own secondary entry point (@angular/cdk/overlay, @angular/cdk/drag-drop, @angular/cdk/a11y, and so on), and every public API is provided as standalone directives or injectable services. You import only what you use, which keeps bundles small.
Overlays and portals
The Overlay module renders floating content — tooltips, menus, popovers, custom dialogs — in a dedicated layer above your app, with smart positioning that flips and shifts to stay on screen. You combine it with the Portal module, which lets you render a template or component into a different DOM location.
import { Component, inject, TemplateRef, viewChild } from '@angular/core';
import { Overlay, OverlayModule } from '@angular/cdk/overlay';
import { TemplatePortal } from '@angular/cdk/portal';
import { ViewContainerRef } from '@angular/core';
@Component({
selector: 'app-menu-trigger',
standalone: true,
imports: [OverlayModule],
template: `
<button (click)="open()" #trigger>Options</button>
<ng-template #menu>
<ul class="menu">
<li>Edit</li>
<li>Duplicate</li>
<li>Delete</li>
</ul>
</ng-template>
`,
})
export class MenuTriggerComponent {
private overlay = inject(Overlay);
private vcr = inject(ViewContainerRef);
private trigger = viewChild.required<HTMLButtonElement>('trigger');
private menu = viewChild.required<TemplateRef<unknown>>('menu');
open(): void {
const positionStrategy = this.overlay
.position()
.flexibleConnectedTo(this.trigger())
.withPositions([
{ originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top' },
]);
const ref = this.overlay.create({
positionStrategy,
scrollStrategy: this.overlay.scrollStrategies.reposition(),
hasBackdrop: true,
});
ref.attach(new TemplatePortal(this.menu(), this.vcr));
ref.backdropClick().subscribe(() => ref.dispose());
}
}
Always call
ref.dispose()(or detach) when the overlay closes. Overlays attach to the global container, so an undisposed overlay leaks DOM and event listeners that the component lifecycle will not clean up for you.
Drag and drop
The drag-drop module turns ordinary elements into draggable items and reorderable lists with a few directives. The cdkDropList directive defines a sortable container, cdkDrag marks each item, and (cdkDropListDropped) fires with the source and target indices so you can mutate your data model.
import { Component, signal } from '@angular/core';
import { CdkDragDrop, DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop';
@Component({
selector: 'app-task-board',
standalone: true,
imports: [DragDropModule],
template: `
<ul cdkDropList (cdkDropListDropped)="drop($event)">
@for (task of tasks(); track task) {
<li cdkDrag>{{ task }}</li>
}
</ul>
`,
})
export class TaskBoardComponent {
tasks = signal(['Design', 'Build', 'Test', 'Ship']);
drop(event: CdkDragDrop<string[]>): void {
this.tasks.update((list) => {
const next = [...list];
moveItemInArray(next, event.previousIndex, event.currentIndex);
return next;
});
}
}
For moving items between lists, connect containers with [cdkDropListConnectedTo] and use transferArrayItem instead of moveItemInArray.
Accessibility utilities
The a11y package solves the parts of accessibility that are easy to get wrong. FocusTrap keeps keyboard focus inside an open dialog, LiveAnnouncer pushes messages to screen readers via an ARIA live region, and FocusMonitor tells you whether an element was focused by keyboard, mouse, or programmatically.
import { Component, inject } from '@angular/core';
import { LiveAnnouncer } from '@angular/cdk/a11y';
@Component({
selector: 'app-save-button',
standalone: true,
template: `<button (click)="save()">Save</button>`,
})
export class SaveButtonComponent {
private announcer = inject(LiveAnnouncer);
save(): void {
// ...persist data...
this.announcer.announce('Document saved', 'polite');
}
}
Virtual scrolling
The scrolling module renders only the rows visible in the viewport, so a list of tens of thousands of items stays smooth. Wrap repeated content in <cdk-virtual-scroll-viewport> and replace @for with *cdkVirtualFor.
<cdk-virtual-scroll-viewport itemSize="48" class="viewport">
<div *cdkVirtualFor="let row of rows" class="row">{{ row.name }}</div>
</cdk-virtual-scroll-viewport>
Module reference
| Entry point | What it provides |
|---|---|
@angular/cdk/overlay | Floating panel rendering and positioning |
@angular/cdk/portal | Render templates/components into a dynamic location |
@angular/cdk/drag-drop | Draggable items and sortable lists |
@angular/cdk/a11y | Focus trapping, live announcements, focus monitoring |
@angular/cdk/scrolling | Virtual scrolling and scroll dispatch |
@angular/cdk/clipboard | Copy text to the clipboard |
@angular/cdk/layout | Responsive breakpoint observation |
Best practices
- Import only the secondary entry points you use so unused features are tree-shaken out of the bundle.
- Dispose every overlay you create — wire
backdropClick()or a detachment toref.dispose()to avoid DOM leaks. - Prefer
flexibleConnectedTowith multiple fallback positions so floating panels stay on screen at any viewport size. - Use
LiveAnnouncerandFocusTrapfor any custom modal or async action; the CDK gives you correct ARIA behavior for free. - Reach for virtual scrolling once a list exceeds a few hundred rows, and set an accurate
itemSizefor stable scrolling. - Treat the CDK as the layer beneath your design system: build styled components on top of its behavior primitives rather than re-implementing positioning and focus logic.