Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

How to use virtual scrolling in Angular

Rendering thousands of items in a list can severely impact application performance, causing slow rendering and sluggish scrolling. With over 10 years of experience building Angular applications and as the creator of CoreUI, I’ve optimized data-heavy admin panels and dashboards where performance is critical. From my expertise, the most effective solution is Angular CDK’s virtual scrolling, which renders only visible items and dramatically improves performance for large datasets. This method can handle lists with tens of thousands of items while maintaining smooth 60fps scrolling.

Use Angular CDK’s cdk-virtual-scroll-viewport to render only visible items in large lists.

import { ScrollingModule } from '@angular/cdk/scrolling'

@Component({
  selector: 'app-user-list',
  standalone: true,
  imports: [ScrollingModule],
  template: `
    <cdk-virtual-scroll-viewport itemSize="50" class="viewport">
      <div *cdkVirtualFor="let user of users" class="item">
        {{ user.name }}
      </div>
    </cdk-virtual-scroll-viewport>
  `,
  styles: [`
    .viewport {
      height: 400px;
      border: 1px solid #ccc;
    }
    .item {
      height: 50px;
      padding: 10px;
    }
  `]
})
export class UserListComponent {
  users = Array.from({length: 10000}, (_, i) => ({
    id: i,
    name: `User ${i}`
  }))
}

How It Works

The cdk-virtual-scroll-viewport creates a scrollable container with a fixed height. The itemSize property tells the viewport that each item is 50px tall. The *cdkVirtualFor directive replaces Angular’s *ngFor and renders only the items currently visible in the viewport plus a small buffer. As you scroll, Angular destroys off-screen elements and creates new ones, keeping memory usage and DOM size minimal.

Installing Angular CDK

First, install the CDK package if you haven’t already:

ng add @angular/cdk

Import the ScrollingModule in your component:

import { ScrollingModule } from '@angular/cdk/scrolling'

@Component({
  // ... component config
  imports: [ScrollingModule, CommonModule]
})

The ng add command installs the package and sets up necessary dependencies. The ScrollingModule provides the virtual scrolling directives and viewport component needed for efficient list rendering.

Dynamic Item Sizes

Handle items with varying heights using template references:

@Component({
  template: `
    <cdk-virtual-scroll-viewport [itemSize]="60" class="viewport">
      <div *cdkVirtualFor="let post of posts" class="post">
        <h3>{{ post.title }}</h3>
        <p>{{ post.excerpt }}</p>
      </div>
    </cdk-virtual-scroll-viewport>
  `,
  styles: [`
    .viewport { height: 500px; }
    .post {
      padding: 15px;
      border-bottom: 1px solid #eee;
    }
  `]
})
export class PostListComponent {
  posts = Array.from({length: 5000}, (_, i) => ({
    title: `Post ${i}`,
    excerpt: `This is the excerpt for post ${i}...`
  }))
}

When items have varying heights, use an average item size for the itemSize property. The viewport uses this as an estimate for scrollbar sizing and buffer calculations. Small variations in actual heights are handled automatically, though very large variations may cause slight scrollbar jumpiness.

Horizontal Virtual Scrolling

Create horizontally scrolling lists:

@Component({
  template: `
    <cdk-virtual-scroll-viewport
      itemSize="200"
      orientation="horizontal"
      class="horizontal-viewport">
      <div *cdkVirtualFor="let item of items" class="horizontal-item">
        {{ item.name }}
      </div>
    </cdk-virtual-scroll-viewport>
  `,
  styles: [`
    .horizontal-viewport {
      width: 800px;
      height: 200px;
      white-space: nowrap;
    }
    .horizontal-item {
      display: inline-block;
      width: 200px;
      height: 100%;
      padding: 20px;
    }
  `]
})
export class HorizontalListComponent {
  items = Array.from({length: 1000}, (_, i) => ({
    id: i,
    name: `Item ${i}`
  }))
}

Setting orientation="horizontal" switches to horizontal scrolling. The itemSize now represents width instead of height. CSS must use inline-block or flex to arrange items horizontally, and the viewport needs a fixed width instead of height.

Scrolling to Specific Items

Programmatically scroll to any position in the list:

@Component({
  template: `
    <button (click)="scrollToIndex(500)">Go to item 500</button>
    <button (click)="scrollToTop()">Back to top</button>

    <cdk-virtual-scroll-viewport
      #viewport
      itemSize="50"
      class="viewport">
      <div *cdkVirtualFor="let item of items; let i = index" class="item">
        {{ i }}: {{ item.name }}
      </div>
    </cdk-virtual-scroll-viewport>
  `
})
export class ScrollableListComponent {
  @ViewChild('viewport') viewport: CdkVirtualScrollViewport

  items = Array.from({length: 10000}, (_, i) => ({ name: `Item ${i}` }))

  scrollToIndex(index: number) {
    this.viewport.scrollToIndex(index)
  }

  scrollToTop() {
    this.viewport.scrollToIndex(0, 'smooth')
  }
}

The ViewChild decorator provides access to the viewport instance. The scrollToIndex() method jumps to a specific item by index. The optional second parameter 'smooth' enables smooth scrolling animation instead of an instant jump.

Handling Scroll Events

React to scroll position changes:

@Component({
  template: `
    <div class="scroll-info">
      Scrolled: {{ scrolledIndex }} / {{ items.length }}
    </div>

    <cdk-virtual-scroll-viewport
      itemSize="50"
      (scrolledIndexChange)="onScrollIndexChange($event)"
      class="viewport">
      <div *cdkVirtualFor="let item of items" class="item">
        {{ item.name }}
      </div>
    </cdk-virtual-scroll-viewport>
  `
})
export class ScrollEventsComponent {
  items = Array.from({length: 10000}, (_, i) => ({ name: `Item ${i}` }))
  scrolledIndex = 0

  onScrollIndexChange(index: number) {
    this.scrolledIndex = index
  }
}

The scrolledIndexChange event emits whenever the viewport scrolls to a different item. This is useful for tracking position, implementing infinite scroll, or showing progress indicators. The index represents the first fully visible item.

Infinite Scrolling with Virtual Scroll

Load more data as users scroll:

@Component({
  template: `
    <cdk-virtual-scroll-viewport
      itemSize="50"
      (scrolledIndexChange)="onScroll($event)"
      class="viewport">
      <div *cdkVirtualFor="let item of items" class="item">
        {{ item.name }}
      </div>
      <div *ngIf="loading" class="loading">Loading...</div>
    </cdk-virtual-scroll-viewport>
  `
})
export class InfiniteScrollComponent {
  items: any[] = []
  loading = false
  page = 0

  ngOnInit() {
    this.loadMore()
  }

  onScroll(index: number) {
    const end = this.viewport.getRenderedRange().end
    const total = this.viewport.getDataLength()

    if (end === total && !this.loading) {
      this.loadMore()
    }
  }

  async loadMore() {
    this.loading = true
    const newItems = await this.fetchItems(this.page++)
    this.items = [...this.items, ...newItems]
    this.loading = false
  }

  async fetchItems(page: number) {
    // Simulate API call
    return Array.from({length: 50}, (_, i) => ({
      name: `Item ${page * 50 + i}`
    }))
  }
}

This checks if the viewport has scrolled to the end using getRenderedRange().end. When the end is reached and no loading is in progress, it fetches more data. The new items are appended to the existing array, and the viewport automatically adjusts.

Best Practice Note

This is the same virtual scrolling approach we use in CoreUI Angular components to handle large datasets efficiently. Virtual scrolling reduces initial render time from seconds to milliseconds for large lists and keeps memory usage constant regardless of total items. For optimal performance, use trackBy functions with *cdkVirtualFor and avoid complex computations in item templates. For drag-and-drop with virtual scrolling, see our guide on how to drag and drop in Angular with CDK which integrates seamlessly with virtual scroll viewports. Consider using CoreUI’s Angular components which include optimized table and list components with built-in virtualization support.


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