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.

For related Angular patterns, check out how to drag and drop in Angular with CDK and how to use virtual scrolling in Angular.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.
CSS Selector for Parent Element
CSS Selector for Parent Element

JavaScript printf equivalent
JavaScript printf equivalent

How to check if an element is visible in JavaScript
How to check if an element is visible in JavaScript

How to loop through a 2D array in JavaScript
How to loop through a 2D array in JavaScript

Answers by CoreUI Core Team