Skip to main content

How to Build a Custom Visualforce Data Table with Pagination in Salesforce

In plain words: Data Table Pagination divides massive Salesforce datasets into bite-sized, sequential pages (e.g., 10 or 25 records per view). Instead of pulling thousands of records into the browser at once—which slows down the page and triggers the 170 KB Visualforce view state limit—pagination fetches only the current page's records on demand using server-side Apex queries or the StandardSetController.

Displaying large datasets in Salesforce without pagination degrades page responsiveness and risks hitting platform limits. Querying thousands of records at once blows past the 50,000 SOQL row retrieval limit, inflates Visualforce View State beyond 170 KB, and creates a frustrating user experience. Implementing structured pagination in Apex and Visualforce gives users fast, predictable navigation while keeping platform resource consumption minimal.

1. Understanding Pagination Architecture in Salesforce

Salesforce developers typically implement pagination using one of two core server-side design patterns:

  • Custom SOQL Limit & Offset: The Apex controller calculates the exact record offset based on the current page number (OFFSET = (pageNum - 1) * pageSize) and queries only the necessary slice of data.
  • StandardSetController: Salesforce's native list controller handles database query chunking, cursor caching, and pagination controls (next(), previous(), getHasNext()) automatically with minimal custom logic.
360 Data Table Pagination Architecture Card:
  • Standard Approach: ApexPages.StandardSetController (supports up to 10,000 records out of the box).
  • Custom SOQL Pattern: LIMIT :pageSize OFFSET :offsetVal (hard SOQL offset limit is 2,000 rows).
  • View State Optimization: Mark table list collections as transient or rely on partial AJAX re-rendering (reRender).
  • Modern UI Standard: Lightning Web Components (<lightning-datatable>) using client-side slicing or wire adapters.

2. Implementing Custom Pagination with Apex & Visualforce

Step 1: Build the Custom Apex Controller
The controller manages page calculation, queries record counts, and applies dynamic LIMIT and OFFSET parameters.
public with sharing class DataTablePaginationController {

    public List<Account> accounts { get; set; }
    public Integer pageSize { get; set; }
    public Integer totalRecords { get; set; }
    public Integer pageNum { get; set; }
    public Integer totalPages { get; set; }

    public DataTablePaginationController() {
        pageSize = 10; // Records per page
        pageNum = 1;   // Starting page
        fetchData();
    }

    public void fetchData() {
        // 1. Get total record count for calculation
        totalRecords = [SELECT COUNT() FROM Account WITH USER_MODE];
        
        // 2. Calculate total pages
        if (totalRecords > 0) {
            totalPages = (Integer) Math.ceil((Decimal) totalRecords / pageSize);
        } else {
            totalPages = 1;
        }

        // 3. Compute query offset
        Integer offsetVal = (pageNum - 1) * pageSize;

        // 4. Retrieve page records safely using SOQL LIMIT and OFFSET
        accounts = [
            SELECT Id, Name, Industry, Type, AnnualRevenue 
            FROM Account 
            WITH USER_MODE 
            ORDER BY Name ASC 
            LIMIT :pageSize 
            OFFSET :offsetVal
        ];
    }

    public void nextPage() {
        if (pageNum < totalPages) {
            pageNum++;
            fetchData();
        }
    }

    public void previousPage() {
        if (pageNum > 1) {
            pageNum--;
            fetchData();
        }
    }

    public void firstPage() {
        pageNum = 1;
        fetchData();
    }

    public void lastPage() {
        pageNum = totalPages;
        fetchData();
    }

    public Boolean getHasNext() {
        return pageNum < totalPages;
    }

    public Boolean getHasPrevious() {
        return pageNum > 1;
    }
}
Step 2: Create the Visualforce Page with Partial Re-rendering
Use reRender on the pagination buttons to update only the table block without a full browser reload.
<apex:page controller="DataTablePaginationController" lightningStylesheets="true">
    <apex:sectionHeader title="Account Directory" subtitle="Paginated Data Table" />

    <apex:form id="tableForm">
        <apex:pageBlock id="dataBlock" title="All Accounts (Total: {!totalRecords})">
            
            <!-- Account Table -->
            <apex:pageBlockTable value="{!accounts}" var="acc">
                <apex:column value="{!acc.Name}"/>
                <apex:column value="{!acc.Industry}"/>
                <apex:column value="{!acc.Type}"/>
                <apex:column value="{!acc.AnnualRevenue}"/>
            </apex:pageBlockTable>

            <!-- Pagination Controls Bar -->
            <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 14px;">
                <div>
                    <span style="font-weight: bold; color: #1f4e79;">
                        Page {!pageNum} of {!totalPages}
                    </span>
                </div>
                
                <div>
                    <apex:commandButton value="« First" action="{!firstPage}" 
                                        disabled="{!!hasPrevious}" reRender="dataBlock" styleClass="slds-button slds-button_neutral" />
                    <apex:commandButton value="‹ Previous" action="{!previousPage}" 
                                        disabled="{!!hasPrevious}" reRender="dataBlock" styleClass="slds-button slds-button_neutral" />
                    <apex:commandButton value="Next ›" action="{!nextPage}" 
                                        disabled="{!!hasNext}" reRender="dataBlock" styleClass="slds-button slds-button_neutral" />
                    <apex:commandButton value="Last »" action="{!lastPage}" 
                                        disabled="{!!hasNext}" reRender="dataBlock" styleClass="slds-button slds-button_neutral" />
                </div>
            </div>

        </apex:pageBlock>
    </apex:form>
</apex:page>

3. StandardSetController: The Native Alternative

For standard pagination tasks, using StandardSetController significantly reduces Apex boilerplate code:

public with sharing class StandardSetPaginationController {
    
    public ApexPages.StandardSetController setCon {
        get {
            if (setCon == null) {
                setCon = new ApexPages.StandardSetController(
                    Database.getQueryLocator([SELECT Id, Name, Industry FROM Account WITH USER_MODE ORDER BY Name])
                );
                setCon.setPageSize(10);
            }
            return setCon;
        }
        set;
    }

    public List<Account> getAccounts() {
        return (List<Account>) setCon.getRecords();
    }
}

4. Common Traps & Platform Limit Pitfalls

Developer Trap: The 2,000 Maximum SOQL OFFSET Limitation
SOQL enforces a maximum OFFSET of 2,000 rows. If a user navigates to page 201 with a page size of 10, the offset becomes 2,010, which throws a fatal runtime exception: NUMBER_OUTSIDE_VALID_RANGE_FOR_TYPE. For datasets exceeding 2,000 rows, use StandardSetController or implement keyset pagination (querying based on WHERE Id > :lastSeenId).
Core Rule: Match your pagination pattern to dataset size: use custom SOQL OFFSET for small datasets under 2,000 records, StandardSetController for up to 10,000 records, and keyset filtering or LWC infinite scrolling for enterprise scale.
  • Fix the LIMIT / OFFSET Syntax Bug: Always write the SOQL clause with limit first, then offset: LIMIT :pageSize OFFSET :offsetVal. Inverting variables or applying offset to limit causes broken queries.
  • Always Use Partial Re-rendering: Ensure pagination action buttons define a valid reRender target ID (like reRender="dataBlock") to prevent full-page refreshes.
  • Enforce User Mode Security: Always append WITH USER_MODE to your SOQL queries to enforce object-level and field-level security automatically.

Summary

Data table pagination is a fundamental requirement for responsive Salesforce enterprise applications. By combining calculated LIMIT and OFFSET SOQL queries with Visualforce partial re-rendering—or by adopting StandardSetController—developers can deliver clean, high-performance table navigation while keeping page execution well within platform governor limits.