Angular, Frontend Development, Infinite Scroll, TypeScript, Standalone Components, Signals, UI/UX, Web Development.

Angular Infinite Scroll Example 2025

User avatar placeholder
Written by Muhammad Talha

August 8, 2025

In modern web applications, especially those displaying large feeds of data like social media timelines, e-commerce product listings, or news articles, providing a smooth user experience is critical. The “load more” button is a thing of the past. Today’s users expect content to load seamlessly as they scroll. This is where infinite scroll comes in.

As of 2025, Angular has evolved significantly, with standalone components and signals becoming the standard for building clean, reactive, and performant applications. This guide will walk you through a modern, step-by-step approach to implementing infinite scroll in Angular using these latest features. We’ll use the highly efficient Intersection Observer API to detect when the user reaches the end of the list, providing a robust solution for your projects.

Let’s get started!


Prerequisites

  • Basic understanding of Angular and TypeScript.
  • Familiarity with Angular Signals.
  • Angular CLI installed on your machine.
  • Node.js and npm/yarn.

Step 1: Setting Up Your Angular Project

First, let’s create a new standalone Angular application. Open your terminal and run:

Bash

ng new angular-infinite-scroll-2025 --standalone
cd angular-infinite-scroll-2025

We don’t need any special libraries beyond what Angular provides, as the Intersection Observer is a native browser API.

Step 2: Creating a Mock Data Service

To simulate fetching data from a server API, we’ll create a simple service that returns paginated data.

Generate a service using the Angular CLI:

Bash

ng generate service services/data

Now, open src/app/services/data.service.ts and add the following code. This service will mimic a network delay and return a new batch of items for each “page.”

TypeScript

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';

export interface Item {
  id: number;
  name: string;
}

@Injectable({
  providedIn: 'root',
})
export class DataService {
  private allItems: Item[] = Array.from({ length: 100 }, (_, i) => ({
    id: i + 1,
    name: `Item #${i + 1}`,
  }));

  getItems(page: number, limit: number): Observable<Item[]> {
    const startIndex = (page - 1) * limit;
    const endIndex = startIndex + limit;
    const items = this.allItems.slice(startIndex, endIndex);

    // Simulate a network delay of 500ms
    return of(items).pipe(delay(500));
  }
}

Step 3: Building the List Component

Next, let’s create the component that will display our list and handle the infinite scroll logic.

Bash

ng generate component components/item-list --standalone

Now, let’s update the component’s TypeScript file at src/app/components/item-list/item-list.component.ts. Here, we’ll use signals to manage our state (the list of items, current page, and loading status).

TypeScript

import { Component, OnInit, ElementRef, ViewChild, OnDestroy, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DataService, Item } from '../../services/data.service';

@Component({
  selector: 'app-item-list',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './item-list.component.html',
  styleUrls: ['./item-list.component.css'],
})
export class ItemListComponent implements OnInit, OnDestroy {
  @ViewChild('sentinel', { static: true }) sentinel!: ElementRef;

  items = signal<Item[]>([]);
  isLoading = signal(false);
  currentPage = signal(1);
  private observer!: IntersectionObserver;
  private readonly ITEMS_PER_PAGE = 20;

  constructor(private dataService: DataService) {}

  ngOnInit(): void {
    this.loadItems();
    this.setupIntersectionObserver();
  }

  ngOnDestroy(): void {
    if (this.observer) {
      this.observer.disconnect();
    }
  }

  private setupIntersectionObserver(): void {
    const options = {
      root: null, // viewport
      rootMargin: '0px',
      threshold: 1.0, // trigger when the sentinel is fully visible
    };

    this.observer = new IntersectionObserver((entries) => {
      if (entries[0].isIntersecting && !this.isLoading()) {
        this.loadItems();
      }
    }, options);

    this.observer.observe(this.sentinel.nativeElement);
  }

  loadItems(): void {
    this.isLoading.set(true);
    this.dataService.getItems(this.currentPage(), this.ITEMS_PER_PAGE).subscribe((newItems) => {
      if (newItems.length > 0) {
        this.items.update(currentItems => [...currentItems, ...newItems]);
        this.currentPage.update(page => page + 1);
      } else {
        // No more items to load, disconnect the observer
        this.observer.disconnect();
      }
      this.isLoading.set(false);
    });
  }
}

Key parts of this code:

  • Signals: items, isLoading, and currentPage are signals, making state management reactive and straightforward.
  • @ViewChild(‘sentinel’): We get a reference to an empty div at the end of our list. This is our trigger element.
  • IntersectionObserver: In setupIntersectionObserver, we create an observer that watches the sentinel. When the sentinel becomes visible in the viewport, it calls our loadItems function. This is far more performant than listening to scroll events.
  • ngOnDestroy: It’s crucial to disconnect the observer when the component is destroyed to prevent memory leaks.

Step 4: Creating the Template and Sentinel

Now, let’s create the HTML for our component in src/app/components/item-list/item-list.component.html.

HTML

<div class="list-container">
  @for (item of items(); track item.id) {
    <div class="item">
      {{ item.name }}
    </div>
  }

  @if (isLoading()) {
    <div class="loading">Loading more items...</div>
  }

  <div #sentinel class="sentinel"></div>
</div>

Add some basic styling in src/app/components/item-list/item-list.component.css to make it look decent.

CSS

.list-container {
  max-width: 600px;
  margin: 20px auto;
  border: 1px solid #ccc;
  padding: 10px;
  border-radius: 8px;
  height: 500px; /* Important: The container needs a fixed height to be scrollable */
  overflow-y: auto;
}

.item {
  padding: 20px;
  border-bottom: 1px solid #eee;
  font-size: 1.2rem;
  text-align: center;
}

.item:last-child {
  border-bottom: none;
}

.loading {
  padding: 20px;
  text-align: center;
  color: #888;
  font-style: italic;
}

.sentinel {
  height: 1px; /* The sentinel needs some height to be observed */
}

Step 5: Putting It All Together

Finally, let’s add our item-list component to our main app component. Open src/app/app.component.ts and import it.

TypeScript

import { Component } from '@angular/core';
import { ItemListComponent } from './components/item-list/item-list.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ItemListComponent], // Import the component here
  template: `
    <main>
      <h1>Angular Infinite Scroll 2025</h1>
      <app-item-list></app-item-list>
    </main>
  `,
  styles: `
    main {
      font-family: sans-serif;
      text-align: center;
    }
  `,
})
export class AppComponent {}

Now, run your application:

Bash

ng serve

Open your browser to http://localhost:4200, and you’ll see a list of items. As you scroll to the bottom, a loading indicator will appear, and new items will be fetched and added to the list automatically.

Conclusion

You have successfully implemented a modern, performant infinite scroll in Angular for 2025! By leveraging standalone components, signals for state management, and the browser’s native Intersection Observer API, you’ve created a solution that is both efficient and provides an excellent user experience. This pattern is perfect for any application that needs to display a large, dynamically growing set of data.

profile picture

Video

Deep Research

Canvas

Image

Gemini can make mistakes, so double-check it

Image placeholder

Lorem ipsum amet elit morbi dolor tortor. Vivamus eget mollis nostra ullam corper. Pharetra torquent auctor metus felis nibh velit. Natoque tellus semper taciti nostra. Semper pharetra montes habitant congue integer magnis.

Leave a Comment