How to use Angular CDK
Angular CDK (Component Dev Kit) provides behavior primitives for building accessible, high-quality UI components without prescribing visual styling. As the creator of CoreUI with 12 years of Angular development experience, I’ve used CDK in applications serving millions of users to build custom drag-drop interfaces, overlays, and virtual scrolling that reduced development time by 50% compared to implementing from scratch.
The most effective approach uses CDK modules for specific behaviors like drag-drop and overlays.
Install Angular CDK
npm install @angular/cdk
Drag and Drop
// app.module.ts
import { DragDropModule } from '@angular/cdk/drag-drop'
@NgModule({
imports: [DragDropModule]
})
export class AppModule {}
// drag-drop.component.ts
import { Component } from '@angular/core'
import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop'
@Component({
selector: 'app-drag-drop',
template: `
<div class="container">
<div class="list">
<h2>To Do</h2>
<div
cdkDropList
#todoList="cdkDropList"
[cdkDropListData]="todo"
[cdkDropListConnectedTo]="[doneList]"
(cdkDropListDropped)="drop($event)"
class="drop-zone"
>
<div *ngFor="let item of todo" cdkDrag class="drag-item">
{{ item }}
</div>
</div>
</div>
<div class="list">
<h2>Done</h2>
<div
cdkDropList
#doneList="cdkDropList"
[cdkDropListData]="done"
[cdkDropListConnectedTo]="[todoList]"
(cdkDropListDropped)="drop($event)"
class="drop-zone"
>
<div *ngFor="let item of done" cdkDrag class="drag-item">
{{ item }}
</div>
</div>
</div>
</div>
`,
styles: [`
.container { display: flex; gap: 20px; }
.list { flex: 1; }
.drop-zone {
min-height: 200px;
background: #f5f5f5;
border: 2px dashed #ccc;
padding: 10px;
}
.drag-item {
padding: 10px;
margin: 5px 0;
background: white;
border: 1px solid #ddd;
cursor: move;
}
.cdk-drag-preview {
box-shadow: 0 5px 10px rgba(0,0,0,0.2);
}
.cdk-drag-animating {
transition: transform 250ms;
}
`]
})
export class DragDropComponent {
todo = ['Write code', 'Review PR', 'Deploy']
done = ['Design mockup']
drop(event: CdkDragDrop<string[]>) {
if (event.previousContainer === event.container) {
moveItemInArray(
event.container.data,
event.previousIndex,
event.currentIndex
)
} else {
transferArrayItem(
event.previousContainer.data,
event.container.data,
event.previousIndex,
event.currentIndex
)
}
}
}
Overlay for Modals and Tooltips
// app.module.ts
import { OverlayModule } from '@angular/cdk/overlay'
@NgModule({
imports: [OverlayModule]
})
export class AppModule {}
// overlay.component.ts
import { Component } from '@angular/core'
import { Overlay, OverlayRef } from '@angular/cdk/overlay'
import { ComponentPortal } from '@angular/cdk/portal'
@Component({
selector: 'app-modal-content',
template: `
<div class="modal">
<h2>Modal Title</h2>
<p>Modal content goes here</p>
<button (click)="close()">Close</button>
</div>
`,
styles: [`
.modal {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 5px 20px rgba(0,0,0,0.3);
min-width: 300px;
}
`]
})
export class ModalContentComponent {
overlayRef: OverlayRef
close() {
this.overlayRef.dispose()
}
}
@Component({
selector: 'app-overlay',
template: `
<button (click)="openModal()">Open Modal</button>
`
})
export class OverlayComponent {
constructor(private overlay: Overlay) {}
openModal() {
const overlayRef = this.overlay.create({
hasBackdrop: true,
backdropClass: 'cdk-overlay-dark-backdrop',
positionStrategy: this.overlay
.position()
.global()
.centerHorizontally()
.centerVertically()
})
const portal = new ComponentPortal(ModalContentComponent)
const componentRef = overlayRef.attach(portal)
componentRef.instance.overlayRef = overlayRef
// Close on backdrop click
overlayRef.backdropClick().subscribe(() => overlayRef.dispose())
}
}
Virtual Scrolling
// app.module.ts
import { ScrollingModule } from '@angular/cdk/scrolling'
@NgModule({
imports: [ScrollingModule]
})
export class AppModule {}
// virtual-scroll.component.ts
import { Component } from '@angular/core'
@Component({
selector: 'app-virtual-scroll',
template: `
<cdk-virtual-scroll-viewport
itemSize="50"
class="viewport"
>
<div *cdkVirtualFor="let item of items" class="item">
{{ item }}
</div>
</cdk-virtual-scroll-viewport>
`,
styles: [`
.viewport {
height: 400px;
border: 1px solid #ccc;
}
.item {
height: 50px;
display: flex;
align-items: center;
padding: 0 20px;
border-bottom: 1px solid #eee;
}
`]
})
export class VirtualScrollComponent {
items = Array.from({ length: 100000 }, (_, i) => `Item #${i}`)
}
Portal for Dynamic Components
// app.module.ts
import { PortalModule } from '@angular/cdk/portal'
@NgModule({
imports: [PortalModule]
})
export class AppModule {}
// portal.component.ts
import { Component, TemplateRef, ViewChild } from '@angular/core'
import { TemplatePortal, ComponentPortal } from '@angular/cdk/portal'
@Component({
selector: 'app-dynamic-content',
template: `<p>Dynamic component content</p>`
})
export class DynamicContentComponent {}
@Component({
selector: 'app-portal',
template: `
<h3>Portal Host</h3>
<div [cdkPortalOutlet]="selectedPortal"></div>
<button (click)="showTemplate()">Show Template</button>
<button (click)="showComponent()">Show Component</button>
<button (click)="clear()">Clear</button>
<ng-template #templateContent>
<p>This is template content</p>
</ng-template>
`
})
export class PortalComponent {
@ViewChild('templateContent') templateRef: TemplateRef<any>
selectedPortal: TemplatePortal<any> | ComponentPortal<any> | null
showTemplate() {
this.selectedPortal = new TemplatePortal(this.templateRef, null)
}
showComponent() {
this.selectedPortal = new ComponentPortal(DynamicContentComponent)
}
clear() {
this.selectedPortal = null
}
}
Accessibility with A11y Module
// app.module.ts
import { A11yModule } from '@angular/cdk/a11y'
@NgModule({
imports: [A11yModule]
})
export class AppModule {}
// focus-trap.component.ts
import { Component } from '@angular/core'
@Component({
selector: 'app-focus-trap',
template: `
<div cdkTrapFocus [cdkTrapFocusAutoCapture]="true" class="dialog">
<h2>Dialog with Focus Trap</h2>
<input placeholder="First input" />
<input placeholder="Second input" />
<button>Submit</button>
<button (click)="close()">Cancel</button>
</div>
`,
styles: [`
.dialog {
padding: 20px;
border: 1px solid #ccc;
background: white;
}
`]
})
export class FocusTrapComponent {
close() {
console.log('Dialog closed')
}
}
Table with Data Source
// app.module.ts
import { CdkTableModule } from '@angular/cdk/table'
@NgModule({
imports: [CdkTableModule]
})
export class AppModule {}
// table.component.ts
import { Component } from '@angular/core'
import { DataSource } from '@angular/cdk/collections'
import { Observable, of } from 'rxjs'
export interface User {
id: number
name: string
email: string
}
export class UserDataSource extends DataSource<User> {
constructor(private users: User[]) {
super()
}
connect(): Observable<User[]> {
return of(this.users)
}
disconnect() {}
}
@Component({
selector: 'app-table',
template: `
<table cdk-table [dataSource]="dataSource" class="table">
<ng-container cdkColumnDef="id">
<th cdk-header-cell *cdkHeaderCellDef>ID</th>
<td cdk-cell *cdkCellDef="let user">{{ user.id }}</td>
</ng-container>
<ng-container cdkColumnDef="name">
<th cdk-header-cell *cdkHeaderCellDef>Name</th>
<td cdk-cell *cdkCellDef="let user">{{ user.name }}</td>
</ng-container>
<ng-container cdkColumnDef="email">
<th cdk-header-cell *cdkHeaderCellDef>Email</th>
<td cdk-cell *cdkCellDef="let user">{{ user.email }}</td>
</ng-container>
<tr cdk-header-row *cdkHeaderRowDef="columns"></tr>
<tr cdk-row *cdkRowDef="let row; columns: columns"></tr>
</table>
`,
styles: [`
.table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #ddd;
}
`]
})
export class TableComponent {
columns = ['id', 'name', 'email']
dataSource = new UserDataSource([
{ id: 1, name: 'John', email: '[email protected]' },
{ id: 2, name: 'Jane', email: '[email protected]' }
])
}
Layout with Breakpoint Observer
// app.module.ts
import { LayoutModule } from '@angular/cdk/layout'
@NgModule({
imports: [LayoutModule]
})
export class AppModule {}
// responsive.component.ts
import { Component, OnInit } from '@angular/core'
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
@Component({
selector: 'app-responsive',
template: `
<div [class]="(layout$ | async)">
<h2>Responsive Layout</h2>
<p *ngIf="(isHandset$ | async)">Mobile view</p>
<p *ngIf="!(isHandset$ | async)">Desktop view</p>
</div>
`
})
export class ResponsiveComponent implements OnInit {
isHandset$: Observable<boolean>
layout$: Observable<string>
constructor(private breakpointObserver: BreakpointObserver) {}
ngOnInit() {
this.isHandset$ = this.breakpointObserver
.observe([Breakpoints.Handset])
.pipe(map(result => result.matches))
this.layout$ = this.breakpointObserver
.observe([
Breakpoints.XSmall,
Breakpoints.Small,
Breakpoints.Medium,
Breakpoints.Large,
Breakpoints.XLarge
])
.pipe(
map(result => {
if (result.breakpoints[Breakpoints.XSmall]) return 'mobile'
if (result.breakpoints[Breakpoints.Small]) return 'tablet'
if (result.breakpoints[Breakpoints.Medium]) return 'laptop'
return 'desktop'
})
)
}
}
Custom Stepper
// app.module.ts
import { CdkStepperModule } from '@angular/cdk/stepper'
@NgModule({
imports: [CdkStepperModule]
})
export class AppModule {}
// stepper.component.ts
import { Component } from '@angular/core'
import { CdkStepper } from '@angular/cdk/stepper'
@Component({
selector: 'app-custom-stepper',
templateUrl: './custom-stepper.component.html',
styleUrls: ['./custom-stepper.component.css'],
providers: [{ provide: CdkStepper, useExisting: CustomStepperComponent }]
})
export class CustomStepperComponent extends CdkStepper {}
<!-- custom-stepper.component.html -->
<div class="stepper">
<div class="steps">
<button
*ngFor="let step of steps; let i = index"
[class.active]="selectedIndex === i"
(click)="selectedIndex = i"
>
Step {{ i + 1 }}
</button>
</div>
<div class="content">
<ng-container [ngTemplateOutlet]="selected.content"></ng-container>
</div>
<div class="navigation">
<button cdkStepperPrevious>Previous</button>
<button cdkStepperNext>Next</button>
</div>
</div>
Best Practice Note
This is how we use Angular CDK across all CoreUI Angular applications for building custom components. Angular CDK provides behavior primitives without prescribing visual styling, allowing full design customization while handling complex functionality like drag-drop, overlays, virtual scrolling, and accessibility. Use drag-drop for reorderable lists, overlay for modals and tooltips, virtual scrolling for large datasets, portal for dynamic components, and A11y module for keyboard navigation and focus management. CDK is the foundation of Angular Material but works standalone for custom designs.
For production applications, consider using CoreUI’s Angular Admin Template which includes CDK-powered custom components.
Related Articles
For related Angular patterns, check out how to drag and drop in Angular with CDK and how to use virtual scrolling in Angular.



