Building Pagination in Vanilla ASP.NET Web Forms

In Vanilla ASP.NET Web Forms, we write (or render) HTML directly for everything. This includes building pagination. The HTML block can be generated through backend C# pre-rendering or frontend JavaScript client-side rendering. Both approaches work, and each has its place depending on your architecture.

This article walks through designing, styling, and implementing a reusable pagination component from scratch.


The Design Phase

In web development, there is typically a theme and typography design phase where one or two complete static HTML pages demonstrate all CSS and global-level JavaScript patterns. This establishes the visual language for the entire application.

The same applies to pagination. Before writing any logic, prepare a static HTML block that represents exactly how your pagination should look. This serves as the reference design for both CSS styling and the dynamic rendering code.

Basic Static Example

A typical pagination block looks like this:

<div>
    <a href='/somepage?page=1'>First</a>
    <span>...</span>
    <a href='/somepage?page=1'>1</a>
    <a href='/somepage?page=2'>2</a>
    <a href='/somepage?page=3'>3</a>
    <a href='/somepage?page=4'>4</a>
    <a href='/somepage?page=5'>5</a>
    <span>...</span>
    <a href='/somepage?page=9'>Last</a>
</div>

Adding CSS Class Definitions

Add class definitions for styling hooks:

<div class='pagination'>
    <a href='/somepage?page=1'>First</a>
    <span>...</span>
    <a href='/somepage?page=1'>1</a>
    <a href='/somepage?page=2'>2</a>
    <a href='/somepage?page=3' class='active'>3</a>
    <a href='/somepage?page=4'>4</a>
    <a href='/somepage?page=5'>5</a>
    <span>...</span>
    <a href='/somepage?page=9'>Last</a>
</div>

Supporting Multiple Themes

To support multiple CSS themes, use different class names on the parent container:

<!-- Named themes -->
<div class='pagination_dark'>
<div class='pagination_light'>
<div class='pagination_blue'>

<!-- Or numbered themes -->
<div class='pagination1'>
<div class='pagination2'>
<div class='pagination3'>

CSS Styling

Here are complete CSS definitions for dark and light themes.

Dark Theme

.pagination_dark {
    margin: 10px 0;
    padding: 5px;
    background: #2d2d2d;
}

.pagination_dark a,
.pagination_dark span {
    display: inline-block;
    line-height: 100%;
    padding: 10px 14px;
    margin: 0 3px;
    text-decoration: none;
    border-radius: 4px;
}

.pagination_dark a {
    background: #505050;
    color: #c0c0c0;
}

.pagination_dark a:hover {
    background: #606060;
    color: #e0e0e0;
}

.pagination_dark a.active {
    background: #0078d4;
    color: #ffffff;
}

.pagination_dark span {
    color: #808080;
    padding: 10px 8px;
}

Light Theme

.pagination_light {
    margin: 10px 0;
    padding: 5px;
    background: #f5f5f5;
}

.pagination_light a,
.pagination_light span {
    display: inline-block;
    line-height: 100%;
    padding: 10px 14px;
    margin: 0 3px;
    text-decoration: none;
    border-radius: 4px;
}

.pagination_light a {
    background: #ffffff;
    color: #333333;
    border: 1px solid #d0d0d0;
}

.pagination_light a:hover {
    background: #e8e8e8;
    color: #111111;
    border-color: #b0b0b0;
}

.pagination_light a.active {
    background: #0078d4;
    color: #ffffff;
    border-color: #0078d4;
}

.pagination_light span {
    color: #999999;
    padding: 10px 8px;
}

Writing the Logic

Understanding the Basic Concept

Before creating a reusable function, let’s understand the fundamental approach. Whether using C# or JavaScript, the pattern is the same: build an HTML string piece by piece.

C# Basic Example:

StringBuilder sb = new StringBuilder();

sb.Append(@"<div class='pagination'>
    <a href='/somepage?page=1'>First</a>
    <span>...</span>");

// The loop
for (int i = 0; i < 5; i++)
{
    int pageno = i + 1;
    sb.Append($"<a href='/somepage?page={pageno}'>{pageno}</a>");
}

sb.Append(@"<span>...</span>
    <a href='/somepage?page=9'>Last</a>
</div>");

string htmlPagination = sb.ToString();

JavaScript Basic Example:

// Use an array as a "builder" for better performance in large loops
const sb = [];

sb.push(`<div class='pagination'>
    <a href='/somepage?page=1'>First</a>
    <span>...</span>`);

for (let i = 0; i < 5; i++) {
    let pageno = i + 1;
    sb.push(`<a href='/somepage?page=${pageno}'>${pageno}</a>`);
}

sb.push(`<span>...</span>
    <a href='/somepage?page=9'>Last</a>
</div>`);

const htmlPagination = sb.join('');

The Reusable Pagination Method

Now let’s transform the basic concept into a universal, reusable function.

C# Implementation

/// <summary>
/// Generates pagination HTML with smart ellipsis handling
/// </summary>
/// <param name="baseUrl">URL with existing query params ending in & (e.g., "/items?cat=5&")</param>
/// <param name="currentPage">The currently active page number</param>
/// <param name="totalPages">Total number of pages available</param>
/// <param name="totalPaginationSlots">How many page numbers to show (default: 7)</param>
/// <param name="cssClass">CSS class for the container (default: "pagination")</param>
/// <returns>Complete pagination HTML string</returns>
public string GeneratePagination(
    string baseUrl,
    int currentPage,
    int totalPages,
    int totalPaginationSlots = 7,
    string cssClass = "pagination")
{
    // Handle edge case: no pages or invalid input
    if (totalPages <= 0) return "";
    if (totalPages == 1) return ""; // No pagination needed for single page

    // Calculate pagination window (centered on current page)
    int half = totalPaginationSlots / 2;
    int startPage = Math.Max(1, currentPage - half);
    if (startPage + totalPaginationSlots - 1 > totalPages)
    {
        startPage = Math.Max(1, totalPages - totalPaginationSlots + 1);
    }

    var sb = new System.Text.StringBuilder();
    sb.Append($"<div class='{cssClass}'>");

    // "First" link - show only if we're not starting from page 1
    if (startPage > 1)
    {
        sb.Append($"<a href='{baseUrl}page=1'>First</a>");
    }

    // Previous ellipsis - show if there's a gap after "First"
    if (startPage > 2)
    {
        sb.Append("<span>...</span>");
    }

    // Page number links
    int endPage = Math.Min(startPage + totalPaginationSlots - 1, totalPages);

    for (int i = startPage; i <= endPage; i++)
    {
        if (i == currentPage)
        {
            sb.Append($"<a href='{baseUrl}page={i}' class='active'>{i}</a>");
        }
        else
        {
            sb.Append($"<a href='{baseUrl}page={i}'>{i}</a>");
        }
    }

    // Next ellipsis - show if there's a gap before "Last"
    if (endPage < totalPages - 1)
    {
        sb.Append("<span>...</span>");
    }

    // "Last" link - show only if we haven't reached the last page
    if (endPage < totalPages)
    {
        sb.Append($"<a href='{baseUrl}page={totalPages}'>Last</a>");
    }

    sb.Append("</div>");
    return sb.ToString();
}

Usage Example:

// Configuration
string baseUrl = "/products?category=electronics&sort=price&";
int currentPage = Convert.ToInt32(Request["page"] ?? "1");
int totalRecords = 420; // From database
int recordsPerPage = 10;
int totalPages = (int)Math.Ceiling((double)totalRecords / recordsPerPage);

// Generate the HTML
string htmlPagination = GeneratePagination(baseUrl, currentPage, totalPages);

// Render to page (example: assign to a Literal control or write directly)
litPagination.Text = htmlPagination;

About the Literal Control

<asp:Literal> is a lightweight pass-through control that simply allows C# backend to output whatever string you assign to its Text property. No extra HTML wrapper, no ViewState, no postback overhead. Unlike interactive server controls (<asp:Button>, <asp:GridView>), it’s purely a rendering placeholder. This makes it ideal for Vanilla Web Forms: you build your HTML string manually, assign it to the Literal, and it renders exactly as it is.

Example of using Literal Control in Front Page:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="ProductList.aspx.cs" Inherits="MyApp.ProductList" %>
<!DOCTYPE html>
<html>
<head>
    <title>Products</title>
    <style>
        /* Dark Theme Pagination */
        .pagination_dark {
            ...
        }
    </style>
</head>
<body>
    <h1>Products</h1>

    <div class="page-info">
        <asp:Literal ID="litPageInfo" runat="server" />
    </div>

    <asp:Literal ID="litProducts" runat="server" />

    <asp:Literal ID="litPagination" runat="server" />
</body>
</html>

JavaScript Implementation

/**
 * Generates pagination HTML with smart ellipsis handling
 * @param {string} baseUrl - URL with existing query params ending in & (e.g., "/items?cat=5&")
 * @param {number} currentPage - The currently active page number
 * @param {number} totalPages - Total number of pages available
 * @param {number} totalPaginationSlots - How many page numbers to show (default: 7)
 * @param {string} cssClass - CSS class for container (default: "pagination")
 * @returns {string} Complete pagination HTML string
 */
function generatePagination(
    baseUrl,
    currentPage,
    totalPages,
    totalPaginationSlots = 7,
    cssClass = 'pagination'
) {
    // Handle edge cases
    if (totalPages <= 0) return '';
    if (totalPages === 1) return ''; // No pagination needed

    // Calculate pagination window (centered on current page)
    const half = Math.floor(totalPaginationSlots / 2);
    let startPage = Math.max(1, currentPage - half);
    if (startPage + totalPaginationSlots - 1 > totalPages) {
        startPage = Math.max(1, totalPages - totalPaginationSlots + 1);
    }

    const html = [];
    html.push(`<div class='${cssClass}'>`);

    // "First" link
    if (startPage > 1) {
        html.push(`<a href='${baseUrl}page=1'>First</a>`);
    }

    // Previous ellipsis
    if (startPage > 2) {
        html.push(`<span>...</span>`);
    }

    // Page number links
    const endPage = Math.min(startPage + totalPaginationSlots - 1, totalPages);

    for (let i = startPage; i <= endPage; i++) {
        if (i === currentPage) {
            html.push(`<a href='${baseUrl}page=${i}' class='active'>${i}</a>`);
        } else {
            html.push(`<a href='${baseUrl}page=${i}'>${i}</a>`);
        }
    }

    // Next ellipsis
    if (endPage < totalPages - 1) {
        html.push(`<span>...</span>`);
    }

    // "Last" link
    if (endPage < totalPages) {
        html.push(`<a href='${baseUrl}page=${totalPages}'>Last</a>`);
    }

    html.push(`</div>`);
    return html.join('');
}

Usage Example:

// Configuration
const baseUrl = '/products?category=electronics&sort=price&';
const currentPage = parseInt(new URLSearchParams(window.location.search).get('page')) || 1;
const totalPages = 42; // Obtained from API response

// Generate and insert into DOM
const paginationHtml = generatePagination(baseUrl, currentPage, totalPages, 7, 'pagination_dark');

document.querySelector('.pagination-container').innerHTML = paginationHtml;

Complete Example with MySQL Database

In real applications, pagination works hand-in-hand with database queries. Here’s how the pieces connect.

The Data Flow

┌─────────────────────────────────────────────────────────────────────────┐
1. User clicks page link: /products?category=5&page=3
└─────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────┐
2. Backend reads parameters: category=5, page=3
└─────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────┐
3. Query #1: Get total count
SELECT COUNT(*) FROM products WHERE category_id = 5
Result: 247 records
└─────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────┐
4. Calculate pagination:                                               │
│     - Records per page: 20
│     - Total pages: CEILING(247 / 20) = 13
│     - Offset: (3 - 1) * 20 = 40
└─────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────┐
5. Query #2: Get page data
SELECT * FROM products WHERE category_id = 5
ORDER BY name LIMIT 40, 20
Result: 20 product records
└─────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────┐
6. Generate pagination HTML using totalPages=13, currentPage=3         │
└─────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────┐
│  7. Render page with product list + pagination controls
└─────────────────────────────────────────────────────────────────────────┘

Backend Implementation (C#)

ProductList.aspx.cs:

using System;
using System.Collections.Generic;
using System.Text;
using MySqlConnector;

public partial class ProductList : System.Web.UI.Page
{
    // Configuration
    const int RecordsPerPage = 20;
    const int PaginationSlots = 7;

    protected void Page_Load(object sender, EventArgs e)
    {
        LoadProducts();
    }

    void LoadProducts()
    {
        // 1. Read parameters
        int categoryId = ParseInt(Request["category"], 0);
        int currentPage = ParseInt(Request["page"], 1);
        if (currentPage < 1) currentPage = 1;

        // 2. Build base URL for pagination links (preserve existing query params)
        string baseUrl = $"/products?category={categoryId}&";

        using (var conn = new MySqlConnection(Config.ConnectionString))
        {
            conn.Open();

            // 3. Get total count
            int totalRecords = 0;
            using (var cmd = new MySqlCommand())
            {
                cmd.Connection = conn;
                cmd.CommandText = @"SELECT COUNT(*) FROM products WHERE category_id = @catId";
                cmd.Parameters.AddWithValue("@catId", categoryId);
                totalRecords = Convert.ToInt32(cmd.ExecuteScalar());
            }

            // 4. Calculate pagination
            int totalPages = (int)Math.Ceiling((double)totalRecords / RecordsPerPage);
            
            // Ensure current page is within valid range
            if (currentPage > totalPages && totalPages > 0)
            {
                currentPage = totalPages;
            }

            int offset = (currentPage - 1) * RecordsPerPage;

            // 5. Get page data
            var products = new List<Product>();
            using (var cmd = new MySqlCommand())
            {
                cmd.Connection = conn;
                cmd.CommandText = @"
                    SELECT product_id, name, price, stock_qty 
                    FROM products 
                    WHERE category_id = @catId
                    ORDER BY name
                    LIMIT @offset, @limit";
                cmd.Parameters.AddWithValue("@catId", categoryId);
                cmd.Parameters.AddWithValue("@offset", offset);
                cmd.Parameters.AddWithValue("@limit", RecordsPerPage);

                using (var reader = cmd.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        products.Add(new Product
                        {
                            ProductId = reader.GetInt32("product_id"),
                            Name = reader.GetString("name"),
                            Price = reader.GetDecimal("price"),
                            StockQty = reader.GetInt32("stock_qty")
                        });
                    }
                }
            }

            // 6. Render product list
            litProducts.Text = RenderProductTable(products);

            // 7. Render pagination
            if (totalPages > 1)
            {
                litPagination.Text = GeneratePagination(
                    baseUrl, currentPage, totalPages, PaginationSlots, "pagination_dark");

                // Also show record count info
                int fromRecord = offset + 1;
                int toRecord = Math.Min(offset + RecordsPerPage, totalRecords);
                litPageInfo.Text = $"Showing {fromRecord}-{toRecord} of {totalRecords} products";
            }
            else
            {
                litPagination.Text = "";
                litPageInfo.Text = $"{totalRecords} product(s) found";
            }
        }
    }

    string RenderProductTable(List<Product> products)
    {
        if (products.Count == 0)
        {
            return "<p>No products found.</p>";
        }

        var sb = new StringBuilder();
        sb.Append("<table class='product-table'>");
        sb.Append("<tr><th>Name</th><th>Price</th><th>Stock</th></tr>");

        foreach (var p in products)
        {
            sb.Append($@"<tr>
                <td>{HtmlEncode(p.Name)}</td>
                <td>{p.Price:C}</td>
                <td>{p.StockQty}</td>
            </tr>");
        }

        sb.Append("</table>");
        return sb.ToString();
    }

    string GeneratePagination(
        string baseUrl,
        int currentPage,
        int totalPages,
        int totalPaginationSlots = 7,
        string cssClass = "pagination")
    {
        if (totalPages <= 1) return "";

        // Calculate pagination window
        int half = totalPaginationSlots / 2;
        int startPage = Math.Max(1, currentPage - half);
        if (startPage + totalPaginationSlots - 1 > totalPages)
        {
            startPage = Math.Max(1, totalPages - totalPaginationSlots + 1);
        }

        var sb = new StringBuilder();
        sb.Append($"<div class='{cssClass}'>");

        if (startPage > 1)
            sb.Append($"<a href='{baseUrl}page=1'>First</a>");

        if (startPage > 2)
            sb.Append("<span>...</span>");

        int endPage = Math.Min(startPage + totalPaginationSlots - 1, totalPages);

        for (int i = startPage; i <= endPage; i++)
        {
            string activeClass = (i == currentPage) ? " class='active'" : "";
            sb.Append($"<a href='{baseUrl}page={i}'{activeClass}>{i}</a>");
        }

        if (endPage < totalPages - 1)
            sb.Append("<span>...</span>");

        if (endPage < totalPages)
            sb.Append($"<a href='{baseUrl}page={totalPages}'>Last</a>");

        sb.Append("</div>");
        return sb.ToString();
    }

    int ParseInt(string value, int defaultValue)
    {
        if (int.TryParse(value, out int result))
            return result;
        return defaultValue;
    }

    string HtmlEncode(string text)
    {
        return System.Web.HttpUtility.HtmlEncode(text ?? "");
    }
}

class Product
{
    public int ProductId { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int StockQty { get; set; }
}

ProductList.aspx:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="ProductList.aspx.cs" Inherits="MyApp.ProductList" %>
<!DOCTYPE html>
<html>
<head>
    <title>Products</title>
    <style>
        /* Dark Theme Pagination */
        .pagination_dark {
            margin: 10px 0;
            padding: 5px;
            background: #2d2d2d;
        }
        .pagination_dark a,
        .pagination_dark span {
            display: inline-block;
            line-height: 100%;
            padding: 10px 14px;
            margin: 0 3px;
            text-decoration: none;
            border-radius: 4px;
        }
        .pagination_dark a {
            background: #505050;
            color: #c0c0c0;
        }
        .pagination_dark a:hover {
            background: #606060;
            color: #e0e0e0;
        }
        .pagination_dark a.active {
            background: #0078d4;
            color: #ffffff;
        }
        .pagination_dark span {
            color: #808080;
            padding: 10px 8px;
        }

        /* Product table */
        .product-table {
            width: 100%;
            border-collapse: collapse;
            margin: 20px 0;
        }
        .product-table th,
        .product-table td {
            padding: 12px;
            text-align: left;
            border-bottom: 1px solid #ddd;
        }
        .product-table th {
            background: #f5f5f5;
        }

        .page-info {
            color: #666;
            font-size: 14px;
            margin: 10px 0;
        }
    </style>
</head>
<body>
    <h1>Products</h1>

    <div class="page-info">
        <asp:Literal ID="litPageInfo" runat="server" />
    </div>

    <asp:Literal ID="litProducts" runat="server" />

    <asp:Literal ID="litPagination" runat="server" />
</body>
</html>

API-Based Implementation (JavaScript Frontend)

For a Fetch API architecture, the frontend handles pagination rendering after receiving data from the API.

ProductListApi.aspx.cs:

using System;
using System.Collections.Generic;
using System.Text.Json;
using MySqlConnector;

public partial class ProductListApi : System.Web.UI.Page
{
    const int RecordsPerPage = 20;

    protected void Page_Load(object sender, EventArgs e)
    {
        try
        {
            string action = (Request["action"] + "").ToLower().Trim();

            switch (action)
            {
                case "get_products": GetProducts(); break;
                default: WriteError("Unknown action", 400); break;
            }
        }
        catch (Exception ex)
        {
            WriteError(ex.Message, 500);
        }

        EndResponse();
    }

    void GetProducts()
    {
        int categoryId = ParseInt(Request["category"], 0);
        int page = ParseInt(Request["page"], 1);
        if (page < 1) page = 1;

        using (var conn = new MySqlConnection(Config.ConnectionString))
        {
            conn.Open();

            // Get total count
            int totalRecords;
            using (var cmd = new MySqlCommand(
                "SELECT COUNT(*) FROM products WHERE category_id = @catId", conn))
            {
                cmd.Parameters.AddWithValue("@catId", categoryId);
                totalRecords = Convert.ToInt32(cmd.ExecuteScalar());
            }

            int totalPages = (int)Math.Ceiling((double)totalRecords / RecordsPerPage);
            if (page > totalPages && totalPages > 0) page = totalPages;
            int offset = (page - 1) * RecordsPerPage;

            // Get page data
            var products = new List<object>();
            using (var cmd = new MySqlCommand(@"
                SELECT product_id, name, price, stock_qty 
                FROM products 
                WHERE category_id = @catId
                ORDER BY name
                LIMIT @offset, @limit", conn))
            {
                cmd.Parameters.AddWithValue("@catId", categoryId);
                cmd.Parameters.AddWithValue("@offset", offset);
                cmd.Parameters.AddWithValue("@limit", RecordsPerPage);

                using (var reader = cmd.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        products.Add(new
                        {
                            product_id = reader.GetInt32("product_id"),
                            name = reader.GetString("name"),
                            price = reader.GetDecimal("price"),
                            stock_qty = reader.GetInt32("stock_qty")
                        });
                    }
                }
            }

            WriteJson(new
            {
                success = true,
                products,
                pagination = new
                {
                    current_page = page,
                    total_pages = totalPages,
                    total_records = totalRecords,
                    records_per_page = RecordsPerPage
                }
            });
        }
    }

    // Helper methods (copy to all API files)
    int ParseInt(string value, int defaultValue)
    {
        return int.TryParse(value, out int result) ? result : defaultValue;
    }

    void EndResponse()
    {
        Response.Flush();
        Response.SuppressContent = true;
        HttpContext.Current.ApplicationInstance.CompleteRequest();
    }

    void WriteJson(object obj)
    {
        Response.ContentType = "application/json";
        Response.Write(JsonSerializer.Serialize(obj));
    }

    void WriteError(string message, int statusCode)
    {
        Response.StatusCode = statusCode;
        WriteJson(new { success = false, message });
    }
}

ProductList.aspx (JavaScript Frontend):

<%@ Page Language="C#" AutoEventWireup="true" 
    CodeBehind="ProductList.aspx.cs" Inherits="MyApp.ProductList" %>
<!DOCTYPE html>
<html>
<head>
    <title>Products</title>
    <style>
        /* Same CSS as before */
        .pagination_dark { /* ... */ }
        .product-table { /* ... */ }
        .page-info { /* ... */ }
    </style>
</head>
<body>
    <h1>Products</h1>

    <div class="page-info" id="pageInfo"></div>
    <div id="productList"></div>
    <div class="pagination-container"></div>

    <script>
        const API_URL = '/ProductListApi.aspx';
        const PAGINATION_SLOTS = 7;

        // Get current parameters from URL
        function getParams() {
            const params = new URLSearchParams(window.location.search);
            return {
                category: parseInt(params.get('category')) || 0,
                page: parseInt(params.get('page')) || 1
            };
        }

        // Load products from API
        async function loadProducts() {
            const { category, page } = getParams();

            const response = await fetch(
                `${API_URL}?action=get_products&category=${category}&page=${page}`
            );
            const data = await response.json();

            if (data.success) {
                renderProducts(data.products);
                renderPagination(data.pagination, category);
                renderPageInfo(data.pagination);
            }
        }

        // Render product table
        function renderProducts(products) {
            if (products.length === 0) {
                document.getElementById('productList').innerHTML = '<p>No products found.</p>';
                return;
            }

            let html = `<table class='product-table'>
                <tr><th>Name</th><th>Price</th><th>Stock</th></tr>`;

            for (const p of products) {
                html += `<tr>
                    <td>${escapeHtml(p.name)}</td>
                    <td>$${p.price.toFixed(2)}</td>
                    <td>${p.stock_qty}</td>
                </tr>`;
            }

            html += '</table>';
            document.getElementById('productList').innerHTML = html;
        }

        // Render pagination
        function renderPagination(pagination, category) {
            const { current_page, total_pages } = pagination;
            
            if (total_pages <= 1) {
                document.querySelector('.pagination-container').innerHTML = '';
                return;
            }
        
            const baseUrl = `/products?category=${category}&`;
        
            const html = generatePagination(
                baseUrl, current_page, total_pages, PAGINATION_SLOTS, 'pagination_dark'
            );
        
            document.querySelector('.pagination-container').innerHTML = html;
        }

        // Pagination generator function
        function generatePagination(baseUrl, currentPage, totalPages, startPage, slots, cssClass) {
            if (totalPages <= 1) return '';

            const html = [];
            html.push(`<div class='${cssClass}'>`);

            if (startPage > 1)
                html.push(`<a href='${baseUrl}page=1'>First</a>`);

            if (startPage > 2)
                html.push(`<span>...</span>`);

            const endPage = Math.min(startPage + slots - 1, totalPages);

            for (let i = startPage; i <= endPage; i++) {
                const activeClass = (i === currentPage) ? " class='active'" : "";
                html.push(`<a href='${baseUrl}page=${i}'${activeClass}>${i}</a>`);
            }

            if (endPage < totalPages - 1)
                html.push(`<span>...</span>`);

            if (endPage < totalPages)
                html.push(`<a href='${baseUrl}page=${totalPages}'>Last</a>`);

            html.push(`</div>`);
            return html.join('');
        }

        // Render page info
        function renderPageInfo(pagination) {
            const { current_page, total_pages, total_records, records_per_page } = pagination;
            const from = (current_page - 1) * records_per_page + 1;
            const to = Math.min(current_page * records_per_page, total_records);

            document.getElementById('pageInfo').textContent = 
                `Showing ${from}-${to} of ${total_records} products`;
        }

        // HTML escape helper
        function escapeHtml(text) {
            const div = document.createElement('div');
            div.textContent = text;
            return div.innerHTML;
        }

        // Initialize
        loadProducts();
    </script>
</body>
</html>

Key Takeaways

  1. Start with static HTML: Design your pagination visually first, then make it dynamic.
  2. CSS theming is simple: Use different class names on the parent container to switch between themes.
  3. The same logic works everywhere: Whether C# or JavaScript, the algorithm is identical—calculate the window of visible pages and generate HTML.
  4. Two database queries: Always query the total count first, then fetch the page data with LIMIT offset, count.
  5. Preserve query parameters: Build your base URL to include existing filters so pagination doesn’t lose context.
  6. Edge cases matter: Handle single pages (no pagination needed), out-of-range pages, and empty results gracefully.

Summary

Building pagination in Vanilla ASP.NET Web Forms is straightforward once you understand the pattern: design the HTML structure, style it with CSS, write a reusable generation function, and connect it to your database queries. Whether you render on the server with C# or on the client with JavaScript, the core logic remains the same.

The key insight is that pagination is just string building. There’s no magic—just loops, conditionals, and a clear understanding of what HTML you want to produce.


Optional: Screen Reader Support

For accessibility, you can add aria-label="Pagination" on the container and aria-current="page" on the active link:

<div class='pagination' aria-label='Pagination'>
    <a href='/products?page=1'>First</a>
    <a href='/products?page=1'>1</a>
    <a href='/products?page=2'>2</a>
    <a href='/products?page=3' class='active' aria-current='page'>3</a>
    <a href='/products?page=4'>4</a>
    <a href='/products?page=5'>Last</a>
</div>

These are ARIA (Accessible Rich Internet Applications) attributes that help screen readers understand your page structure for visually impaired users. The aria-label tells screen readers “this is a pagination navigation block,” while aria-current="page" indicates which page is currently active—so the screen reader announces “current page, 3” instead of just “link, 3.”

For public-facing websites, accessibility compliance is often legally required in some jurisdictions, and it’s generally good practice regardless. However, for private sector and business in-house applications, this is typically not applicable and can be skipped.