Loading large datasets into a single view slows down page render speeds, clutters the UI, and risks exhausting browser memory. Implementing clean, responsive pagination inside your Lightning Web Components (LWC) delivers a crisp user experience while keeping data retrieval well within Salesforce platform limits.
1. Apex Controller with Server-Side Paging
The backend Apex controller queries records using dynamic LIMIT and OFFSET parameters, wrapping the records alongside the total count into a single response object:
public with sharing class ItemPaginationController {
public class PagedResult {
@AuraEnabled public Integer totalCount { get; set; }
@AuraEnabled public List<Account> records { get; set; }
}
@AuraEnabled(cacheable=true)
public static PagedResult getItems(Integer pageNumber, Integer pageSize) {
PagedResult result = new PagedResult();
// Calculate offset safely
Integer offsetRows = (pageNumber - 1) * pageSize;
result.totalCount = [SELECT COUNT() FROM Account WITH USER_MODE];
result.records = [
SELECT Id, Name, Industry, Type, CreatedDate
FROM Account
WITH USER_MODE
ORDER BY Name ASC
LIMIT :pageSize
OFFSET :offsetRows
];
return result;
}
}
2. LWC JavaScript Controller (pagination.js)
The JavaScript controller uses the reactive @wire service. Whenever the reactive parameter '$currentPage' updates, the wire adapter automatically retrieves the new page slice from the server:
import { LightningElement, wire } from 'lwc';
import getItems from '@salesforce/apex/ItemPaginationController.getItems';
const PAGE_SIZE = 10;
export default class Pagination extends LightningElement {
displayItems = [];
currentPage = 1;
totalPages = 1;
totalRecords = 0;
pageNumbers = [];
error;
@wire(getItems, { pageNumber: '$currentPage', pageSize: PAGE_SIZE })
wiredItems({ error, data }) {
if (data) {
this.displayItems = data.records;
this.totalRecords = data.totalCount;
this.totalPages = Math.ceil(data.totalCount / PAGE_SIZE) || 1;
// Build dynamic numeric button array
this.pageNumbers = Array.from({ length: this.totalPages }, (_, i) => ({
number: i + 1,
variant: (i + 1 === this.currentPage) ? 'brand' : 'neutral'
}));
this.error = undefined;
} else if (error) {
this.error = error;
this.displayItems = [];
}
}
get isFirstPage() {
return this.currentPage <= 1;
}
get isLastPage() {
return this.currentPage >= this.totalPages;
}
handlePrevious() {
if (this.currentPage > 1) {
this.currentPage--;
}
}
handleNext() {
if (this.currentPage < this.totalPages) {
this.currentPage++;
}
}
handlePageSelect(event) {
this.currentPage = parseInt(event.target.value, 10);
}
}
3. HTML Template (pagination.html)
The template displays items using modern Salesforce Lightning Design System (SLDS) cards, list items, and lightning button controls:
<template>
<lightning-card title="Account Directory" icon-name="standard:account">
<div class="slds-p-around_medium">
<!-- Record List -->
<template lwc:if={displayItems.length}>
<ul class="slds-has-dividers_bottom-space">
<template for:each={displayItems} for:item="item">
<li key={item.Id} class="slds-item slds-p-vertical_x-small">
<div class="slds-grid slds-grid_align-spread">
<span class="slds-text-heading_small slds-truncate">{item.Name}</span>
<span class="slds-badge">{item.Industry}</span>
</div>
</li>
</template>
</ul>
</template>
<template lwc:elseif={error}>
<div class="slds-notify slds-notify_alert slds-theme_alert-texture slds-theme_error">
<h2>Error loading records. Please check permissions.</h2>
</div>
</template>
<!-- Pagination Controls Bar -->
<div class="slds-grid slds-grid_align-center slds-m-top_medium slds-grid_vertical-align-center">
<!-- Previous Button -->
<lightning-button-icon
icon-name="utility:chevronleft"
variant="border-filled"
alternative-text="Previous Page"
title="Previous"
disabled={isFirstPage}
onclick={handlePrevious}
class="slds-m-right_x-small">
</lightning-button-icon>
<!-- Page Number Buttons -->
<template for:each={pageNumbers} for:item="page">
<lightning-button
key={page.number}
label={page.number}
value={page.number}
variant={page.variant}
onclick={handlePageSelect}
class="slds-m-horizontal_xx-small">
</lightning-button>
</template>
<!-- Next Button -->
<lightning-button-icon
icon-name="utility:chevronright"
variant="border-filled"
alternative-text="Next Page"
title="Next"
disabled={isLastPage}
onclick={handleNext}
class="slds-m-left_x-small">
</lightning-button-icon>
</div>
<!-- Status Indicator -->
<p class="slds-text-align_center slds-text-color_weak slds-m-top_x-small slds-text-body_small">
Page {currentPage} of {totalPages} ({totalRecords} Total Records)
</p>
</div>
</lightning-card>
</template>
- Step 1: Expose an
@AuraEnabled(cacheable=true)method in Apex returning a wrapper with items and total record count. - Step 2: Declare a reactive
currentPageproperty in LWC and bind it to the wire adapter parameter. - Step 3: Calculate total pages using
Math.ceil(totalCount / pageSize)and generate an array of button objects. - Step 4: Update
currentPagevia button click handlers; the wire service handles data refreshing automatically.
4. Critical Architecture Trap: SOQL Offset Limit
OFFSET in Salesforce is 2,000 rows. If your dataset exceeds 2,000 records, standard server-side offset pagination will fail. For large datasets, use Keyset Pagination (querying with WHERE Id > :lastSeenId ORDER BY Id ASC LIMIT :pageSize) or client-side caching.
5. Pagination Strategy Comparison
- Client-Side Slicing: Retrieve all records once (under 1,000 rows) and slice with
data.slice(start, end)in JavaScript. Offers instant page switches with zero extra server calls. - Server-Side Offset: Best for medium tables (100 to 2,000 rows) where initial load payload must stay small.
- Keyset Paging / Infinite Scroll: The enterprise standard for large datasets (> 2,000 rows) and mobile-optimized feeds using
lightning-datatable.
@AuraEnabled(cacheable=true) to get automatic client-side caching and responsive page navigation out of the box.
Summary
Adding pagination to your Lightning Web Components transforms dense data tables into fast, scannable user interfaces. By using the reactive wire service, SLDS styling tokens, and clean button controls, you ensure your custom components stay performant, accessible, and easy to maintain.