How to paginate tables in Angular
Pagination is essential for displaying large datasets in manageable chunks, improving performance and user experience. As the creator of CoreUI with over 10 years of Angular experience since 2014, I’ve implemented pagination for tables handling millions of records in enterprise applications. The most effective approach uses component state to track current page and page size, with computed properties to slice data for display. This provides smooth navigation through large datasets.
Create basic pagination with page controls.
import { Component } from '@angular/core'
import { CommonModule } from '@angular/common'
@Component({
selector: 'app-paginated-table',
standalone: true,
imports: [CommonModule],
template: `
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of paginatedItems">
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
<td>{{ item.email }}</td>
</tr>
</tbody>
</table>
<div class="pagination">
<button (click)="previousPage()" [disabled]="currentPage === 1">Previous</button>
<span>Page {{ currentPage }} of {{ totalPages }}</span>
<button (click)="nextPage()" [disabled]="currentPage === totalPages">Next</button>
</div>
`
})
export class PaginatedTableComponent {
items = Array.from({ length: 100 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`
}))
currentPage = 1
pageSize = 10
get totalPages() {
return Math.ceil(this.items.length / this.pageSize)
}
get paginatedItems() {
const start = (this.currentPage - 1) * this.pageSize
return this.items.slice(start, start + this.pageSize)
}
nextPage() {
if (this.currentPage < this.totalPages) {
this.currentPage++
}
}
previousPage() {
if (this.currentPage > 1) {
this.currentPage--
}
}
}
The paginatedItems getter slices the array based on current page. The totalPages calculates total pages needed. Navigation buttons update the current page. Disabled buttons prevent invalid navigation.
Adding Page Size Selector
Allow users to choose items per page.
import { Component } from '@angular/core'
import { CommonModule } from '@angular/common'
import { FormsModule } from '@angular/forms'
@Component({
selector: 'app-table-with-page-size',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<table>
<tbody>
<tr *ngFor="let item of paginatedItems">
<td>{{ item.name }}</td>
</tr>
</tbody>
</table>
<div class="pagination">
<select [(ngModel)]="pageSize" (change)="onPageSizeChange()">
<option [value]="5">5 per page</option>
<option [value]="10">10 per page</option>
<option [value]="25">25 per page</option>
<option [value]="50">50 per page</option>
</select>
<button (click)="previousPage()" [disabled]="currentPage === 1">Previous</button>
<span>{{ currentPage }} / {{ totalPages }}</span>
<button (click)="nextPage()" [disabled]="currentPage === totalPages">Next</button>
</div>
`
})
export class TableWithPageSizeComponent {
items = Array.from({ length: 100 }, (_, i) => ({ id: i + 1, name: `Item ${i + 1}` }))
currentPage = 1
pageSize = 10
get totalPages() {
return Math.ceil(this.items.length / this.pageSize)
}
get paginatedItems() {
const start = (this.currentPage - 1) * this.pageSize
return this.items.slice(start, start + this.pageSize)
}
nextPage() {
if (this.currentPage < this.totalPages) this.currentPage++
}
previousPage() {
if (this.currentPage > 1) this.currentPage--
}
onPageSizeChange() {
this.currentPage = 1
}
}
The select dropdown binds to pageSize. Changing page size resets to page 1. This prevents showing invalid page numbers after reducing page size.
Implementing Server-Side Pagination
Fetch paginated data from an API.
import { Component, OnInit } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { CommonModule } from '@angular/common'
@Component({
selector: 'app-server-paginated-table',
standalone: true,
imports: [CommonModule],
template: `
<div *ngIf="loading">Loading...</div>
<table *ngIf="!loading">
<tbody>
<tr *ngFor="let item of items">
<td>{{ item.name }}</td>
</tr>
</tbody>
</table>
<div class="pagination">
<button (click)="loadPage(currentPage - 1)" [disabled]="currentPage === 1">Previous</button>
<span>{{ currentPage }} / {{ totalPages }}</span>
<button (click)="loadPage(currentPage + 1)" [disabled]="currentPage === totalPages">Next</button>
</div>
`
})
export class ServerPaginatedTableComponent implements OnInit {
items: any[] = []
currentPage = 1
pageSize = 10
totalPages = 0
loading = false
constructor(private http: HttpClient) {}
ngOnInit() {
this.loadPage(1)
}
loadPage(page: number) {
this.loading = true
this.http.get<any>(`/api/items?page=${page}&size=${this.pageSize}`)
.subscribe({
next: (response) => {
this.items = response.items
this.currentPage = response.page
this.totalPages = response.totalPages
this.loading = false
},
error: () => {
this.loading = false
}
})
}
}
The API returns paginated data with metadata. Each page navigation triggers a new API call. The server handles filtering, sorting, and pagination. This approach works with massive datasets.
Adding Page Number Buttons
Show clickable page numbers.
@Component({
template: `
<div class="pagination">
<button (click)="goToPage(1)" [disabled]="currentPage === 1">First</button>
<button (click)="goToPage(currentPage - 1)" [disabled]="currentPage === 1">Previous</button>
<button
*ngFor="let page of visiblePages"
(click)="goToPage(page)"
[class.active]="page === currentPage">
{{ page }}
</button>
<button (click)="goToPage(currentPage + 1)" [disabled]="currentPage === totalPages">Next</button>
<button (click)="goToPage(totalPages)" [disabled]="currentPage === totalPages">Last</button>
</div>
`
})
export class TableWithPageNumbersComponent {
items = Array.from({ length: 100 }, (_, i) => ({ id: i + 1, name: `Item ${i + 1}` }))
currentPage = 1
pageSize = 10
get totalPages() {
return Math.ceil(this.items.length / this.pageSize)
}
get visiblePages(): number[] {
const pages: number[] = []
const maxVisible = 5
let start = Math.max(1, this.currentPage - Math.floor(maxVisible / 2))
let end = Math.min(this.totalPages, start + maxVisible - 1)
if (end - start < maxVisible - 1) {
start = Math.max(1, end - maxVisible + 1)
}
for (let i = start; i <= end; i++) {
pages.push(i)
}
return pages
}
get paginatedItems() {
const start = (this.currentPage - 1) * this.pageSize
return this.items.slice(start, start + this.pageSize)
}
goToPage(page: number) {
if (page >= 1 && page <= this.totalPages) {
this.currentPage = page
}
}
}
The visiblePages getter shows a window of page numbers around the current page. This prevents showing hundreds of buttons for large datasets. First and Last buttons provide quick navigation to extremes.
Best Practice Note
This is the same pagination pattern we use in CoreUI Angular tables for managing large datasets efficiently. For client-side pagination with under 1000 items, slice arrays in memory. For larger datasets, implement server-side pagination to reduce payload size. Always show total count and current range (e.g., “Showing 11-20 of 243”) for user context. Consider using CoreUI’s CTable component which includes built-in pagination with all these features, saving development time and ensuring consistent UX.



