Skip to main content

Building a Reusable Paginator in Salesforce LWC: Complete Component Guide

In plain words: A Paginator Component in Lightning Web Components (LWC) is a modular navigation control that splits large lists of records into manageable pages. It provides intuitive navigation buttons (First, Previous, Next, Last) and informs the parent component of page selections via standard JavaScript CustomEvent dispatches.

Displaying hundreds of records at once on a Salesforce page slows down browser performance and overwhelms users. A dedicated child paginator component cleanly separates the user interface navigation controls from data retrieval logic, allowing you to reuse the exact same pagination toolbar across lists, data tables, and custom cards.

1. Component Architecture & Data Flow

The paginator architecture follows standard reactive parent-child communication patterns in LWC:

  • Parent-to-Child Data (@api Properties): The parent component passes currentPage, totalItems, and pageSize down into the child paginator.
  • Child Computed State: Getters in the child determine total pages and compute disabled button states (isFirstPage, isLastPage) to prevent out-of-bounds navigation.
  • Child-to-Parent Notification (CustomEvent): When a user clicks any navigation button, the child dispatches a pagechange event carrying the targeted page number in event.detail.
360 Paginator Architecture Blueprint:
  • Child Inputs: @api currentPage, @api totalItems, @api pageSize.
  • Event Dispatch: new CustomEvent('pagechange', { detail: newPageNumber }).
  • Parent Action: Listens via onpagechange={handlePageChange} and fetches or slices data for the active page.
  • UI Standard: Salesforce Lightning Design System (SLDS) button groups for responsive alignment.

2. Implementing the Child Paginator Component

Below is the complete implementation for the reusable child component, styled cleanly with SLDS utilities.

Step 1: Child HTML Template (paginator.html)
<template>
    <div class="slds-align_absolute-center slds-m-vertical_medium">
        <lightning-button-group>
            <lightning-button
                label="First"
                icon-name="utility:jump_to_left"
                disabled={isFirstPage}
                onclick={handleFirstPage}>
            </lightning-button>
            <lightning-button
                label="Previous"
                icon-name="utility:chevronleft"
                disabled={isFirstPage}
                onclick={handlePreviousPage}>
            </lightning-button>
        </lightning-button-group>

        <span class="slds-p-horizontal_medium slds-text-body_regular">
            Page <strong>{currentPage}</strong> of <strong>{totalPages}</strong>
        </span>

        <lightning-button-group>
            <lightning-button
                label="Next"
                icon-name="utility:chevronright"
                icon-position="right"
                disabled={isLastPage}
                onclick={handleNextPage}>
            </lightning-button>
            <lightning-button
                label="Last"
                icon-name="utility:jump_to_right"
                icon-position="right"
                disabled={isLastPage}
                onclick={handleLastPage}>
            </lightning-button>
        </lightning-button-group>
    </div>
</template>
Step 2: Child JavaScript Controller (paginator.js)
import { LightningElement, api } from 'lwc';

export default class Paginator extends LightningElement {
    @api currentPage = 1;
    @api totalItems = 0;
    @api pageSize = 10;

    get totalPages() {
        if (!this.totalItems || !this.pageSize) {
            return 1;
        }
        return Math.ceil(this.totalItems / this.pageSize);
    }

    get isFirstPage() {
        return this.currentPage <= 1;
    }

    get isLastPage() {
        return this.currentPage >= this.totalPages;
    }

    handleFirstPage() {
        if (!this.isFirstPage) {
            this.dispatchPageChangeEvent(1);
        }
    }

    handlePreviousPage() {
        if (!this.isFirstPage) {
            this.dispatchPageChangeEvent(this.currentPage - 1);
        }
    }

    handleNextPage() {
        if (!this.isLastPage) {
            this.dispatchPageChangeEvent(this.currentPage + 1);
        }
    }

    handleLastPage() {
        if (!this.isLastPage) {
            this.dispatchPageChangeEvent(this.totalPages);
        }
    }

    dispatchPageChangeEvent(targetPage) {
        if (targetPage !== this.currentPage) {
            const pageChangeEvent = new CustomEvent('pagechange', {
                detail: targetPage
            });
            this.dispatchEvent(pageChangeEvent);
        }
    }
}

3. Embedding the Paginator in a Parent Component

The parent component maintains the master dataset, slices or queries data for the active page, and responds to the child's pagechange event.

Step 3: Parent HTML Template (paginationContainer.html)
<template>
    <lightning-card title="Account Directory" icon-name="standard:account">
        <div class="slds-p-around_medium">
            <!-- Render your paged data table or custom cards -->
            <lightning-datatable
                key-field="Id"
                data={displayedRecords}
                columns={columns}
                hide-checkbox-column>
            </lightning-datatable>

            <!-- Reusable Child Paginator -->
            <c-paginator
                current-page={currentPage}
                total-items={totalRecords}
                page-size={pageSize}
                onpagechange={handlePageChange}>
            </c-paginator>
        </div>
    </lightning-card>
</template>
Step 4: Parent JavaScript Controller (paginationContainer.js)
import { LightningElement, track } from 'lwc';

const COLUMNS = [
    { label: 'Name', fieldName: 'Name', type: 'text' },
    { label: 'Industry', fieldName: 'Industry', type: 'text' },
    { label: 'Phone', fieldName: 'Phone', type: 'phone' }
];

export default class PaginationContainer extends LightningElement {
    columns = COLUMNS;
    @track allRecords = [];
    @track displayedRecords = [];

    currentPage = 1;
    pageSize = 5;

    get totalRecords() {
        return this.allRecords.length;
    }

    connectedCallback() {
        this.loadSampleData();
        this.updatePageData();
    }

    loadSampleData() {
        const records = [];
        for (let i = 1; i <= 32; i++) {
            records.push({
                Id: `ACC-${i}`,
                Name: `United Partners Corp ${i}`,
                Industry: i % 2 === 0 ? 'Technology' : 'Finance',
                Phone: `(555) 019-${1000 + i}`
            });
        }
        this.allRecords = records;
    }

    handlePageChange(event) {
        this.currentPage = event.detail;
        this.updatePageData();
    }

    updatePageData() {
        const startIndex = (this.currentPage - 1) * this.pageSize;
        const endIndex = startIndex + this.pageSize;
        this.displayedRecords = this.allRecords.slice(startIndex, endIndex);
    }
}

4. Common Traps & Best Practices

Reactivity Trap: Forgetting to Reset the Page on Search or Filter Actions
If a user is on page 5 and performs a search that narrows total records down to 3, the component will remain on page 5 and display an empty screen. Always reset this.currentPage = 1 whenever the master dataset is filtered.
Core Rule: Keep pagination controls completely decoupled from your backend schemas. Handle button states in the child and manage data array slicing (Array.slice()) or server queries in the parent.
  • Client vs. Server-Side Processing: Use client-side array slicing for datasets under 1,000 rows. For high-volume enterprise datasets, query Apex with OFFSET or keyset pagination on every page change.
  • Use Built-in SLDS Classes: Use standard Lightning Design System classes (slds-align_absolute-center, slds-button-group) rather than writing custom margin and alignment CSS.
  • Guard Against Division by Zero: Always ensure totalPages defaults to at least 1 when datasets are empty to avoid rendering Page 1 of 0.

Summary

Decoupling pagination controls into a standalone child component creates a reusable, scalable architecture for Lightning Web Components. By combining standard @api properties, reactive getters, and CustomEvents, developers can deliver clean, responsive record navigation across all Salesforce applications.