Pagination Table — Free HTML CSS JS Snippet

Pagination Table · Tables · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Client-side pagination: data.slice(start,end) with computed start/end per page
Smart page numbers: Set of [1, pages, page-1, page, page+1] with ellipsis gaps
Prev/Next buttons: disabled at boundaries via button.disabled property
Per-page selector: 5/10/20 options, resets to page 1 on change
Showing X–Y of Z info bar: updates dynamically on every render
Status badge: Active (green) / Inactive (grey) via .active/.inactive class
tbody regenerated via innerHTML on each render — simple, readable pattern
Responsive: overflow-x:auto on table wrapper

About this UI Snippet

Pagination Table — Client-Side Pagination, Smart Page Numbers, Per-Page Select & Showing X–Y of Z

Screenshot of the Pagination Table snippet rendered live

Pagination is essential for any data table that displays more rows than fit on one screen. Without it, users must scroll through hundreds of rows to find what they need. With well-implemented pagination, users can navigate between pages, control the number of rows shown, and see exactly where they are in the dataset. This snippet provides a complete client-side paginated table: smart page number buttons with ellipsis, prev/next navigation, a rows-per-page selector, and a "Showing X–Y of Z results" info display.

How client-side pagination works

The full dataset (48 sample rows) is stored in a JavaScript array. The render() function computes the current page slice: start = (page-1) * perPage, end = min(start + perPage, total). data.slice(start, end) extracts the visible rows. The function rebuilds the tbody innerHTML and the pagination controls on every page change.

Smart page number range with ellipsis

Instead of showing all page numbers (which would overflow for large datasets), the pagination shows: page 1 (always), pages around the current page (current-1, current, current+1), and the last page (always). Gaps between these numbers are filled with an ellipsis "…" element. A Set deduplicates and sorts the visible page numbers. This pattern matches the standard pagination used by Google, GitHub, and most admin dashboards.

Rows per page selector

A select element offers 5, 10, and 20 rows. Changing the selection calls setPerPage() which updates the perPage variable, resets page to 1, and calls render(). This ensures the user always sees page 1 after changing the row count — they are not left on a page that no longer exists.

The info bar

"Showing 1–10 of 48 results" gives users exact orientation within the dataset. The numbers update dynamically: the start of the current slice (1-indexed), the end of the current slice, and the total count. This pattern appears in virtually every production data table interface.

Customising with real data

Replace the data array with your real data from an API. For server-side pagination, change the fetch call to include page and perPage as query parameters. The render() function receives the total count from the API response header (X-Total-Count or paginated response body) and computes page buttons from that total.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't have to hand-simulate the page-range algorithm to understand it. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how the Set built from 1, pages, page-1, page, and page+1 produces the visible page-number buttons, and why sorting and diffing consecutive entries in that Set is what decides where an ellipsis gets inserted. The same assistant can help optimize it, for example asking whether rebuilding the entire tbody innerHTML on every page change is fine for 48 rows but should switch to patching only changed rows if the dataset grows into the thousands. It's also useful for extending the table: ask it to add a search input that filters the data array before slicing so pagination and the results count stay correct together, wire data.slice up to a real fetch call for server-side pagination, or add column sorting that runs before the page slice is taken. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a client-side "paginated data table" in plain HTML, CSS, and JavaScript, with no pagination library — pagination must be computed purely from array slicing and a Set-based page-range algorithm.

Requirements:
- An in-memory array of row objects (id, name, email, role, status) as the full dataset, plus a page variable and a perPage variable, both starting at sensible defaults.
- A single render function that computes start as (page minus 1) times perPage and end as the minimum of start plus perPage and the total row count, takes data.slice(start, end) as the currently visible rows, and rewrites the table body's rows from that slice.
- The same render function must rebuild the pagination controls: a Previous button disabled on the first page, a Next button disabled on the last page, and page-number buttons generated from a Set containing exactly the first page, the last page, and the current page along with its immediate neighbor page numbers, deduplicated and sorted; whenever two consecutive numbers in that sorted set differ by more than 1, insert an ellipsis element between their buttons instead of every number in between.
- A rows-per-page select control (offering at least three options) that, on change, updates perPage, resets page back to 1, and calls render — the row count must never leave the user stranded on a now-nonexistent page.
- An info line above the table that reads "Showing X to Y of Z results", recomputed from the same start/end/total values on every render.
- Give each row a status badge (e.g. Active/Inactive) styled differently by status, and make the table wrapper horizontally scrollable for narrow viewports.

Step by step

How to Use

  1. 1
    Click page numbers or Prev/Next to navigateThe table re-renders to the selected page. The page number button gets an active indigo style. Prev and Next buttons disable when at the first or last page respectively.
  2. 2
    Change rows per pageSelect 5, 10, or 20 from the dropdown. The table resets to page 1 with the new row count. The "Showing X–Y of Z" info updates to reflect the new per-page slice.
  3. 3
    Replace the sample data arrayIn the JS panel, replace the data array at the top with your own data. Each object needs the properties displayed in the table columns. The pagination and info bar update automatically from data.length.
  4. 4
    Implement server-side paginationReplace data.slice(start,end) with a fetch call: fetch("/api/users?page="+page+"&limit="+perPage). Use the API response total count to compute pages: pages = Math.ceil(totalFromApi / perPage). Render the API rows into the tbody.
  5. 5
    Add column sortingSee the Sortable Table snippet in this library. Both use the same table structure. Add data-col and data-type attributes to th elements and the sortBy() function to sort the data array before slicing for the current page.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component with useState for page and perPage and a useMemo for the visible slice, or "Tailwind" for a Tailwind CSS version.

Real-world uses

Common Use Cases

User management and subscriber list tables in admin panels
Paginate user records, customer lists, or subscriber databases — typically the same rows rendered by the Data Table snippet, with expandable detail rows for drill-down. The per-page selector lets admins switch between a quick-scan view (20 rows) and a detailed review mode (5 rows). The info bar confirms how many total records exist.
Order history and transaction list tables
Paginate order tables where users review past purchases or admins process fulfilment queues. The smart page number range lets users jump to specific pages (e.g., page 5 of 12 to find an order from last month) without clicking through each page.
Analytics report and data export tables
Show analytics events, conversion logs, or error reports in paginated form. The per-page selector helps analysts who want to review all events on one page (20 rows) versus engineers who want a quick overview (5 rows) with each row in detail.
Server-side pagination for large datasets
Adapt for server-side pagination by replacing data.slice() with an API fetch. Pass page and perPage as query parameters. Receive total count from the API. The pagination UI is identical whether data is client-side or server-side — only the data fetch changes.
Study the smart page number range algorithm
The Set-based page number range algorithm — [1, pages, page-1, page, page+1].filter() — is a compact implementation of the standard pagination range pattern. Understanding it teaches how to show relevant page numbers without overflow and insert ellipsis for gaps.
Product catalogue and inventory listing tables
Paginate product listings in a back-office inventory tool or a B2B product catalogue. Combine with the column sort from the Sortable Table snippet for a sort + paginate data grid. The per-page selector gives buyers control over browsing density.

Got questions?

Frequently Asked Questions

The algorithm creates a Set containing: 1 (always show first page), pages (always show last page), and page-1, page, page+1 (show current page and neighbours). It filters out values below 1 or above pages. The Set deduplicates (e.g., if page=1, both 1 and page-1=0 are in the input but only 1 makes it through the filter). The sorted array is iterated, and when a gap greater than 1 exists between consecutive numbers, an ellipsis element is inserted.

Remove the data array and data.slice() call. In render(), add: const res = await fetch("/api/rows?page="+page+"&limit="+perPage); const { rows, total } = await res.json(); const pages = Math.ceil(total / perPage). Render rows to the tbody from the API response. Update the info bar from total. The pagination button generation code stays exactly the same — it only needs total and pages, which now come from the API response instead of data.length.

Add a search input above the table. On every input event, filter the data array: const filtered = data.filter(r => r.name.toLowerCase().includes(q) || r.email.toLowerCase().includes(q)). Reset page to 1 on each search. Use filtered instead of data in the render() calculations: start = (page-1)*perPage, rows = filtered.slice(start, end), pages = Math.ceil(filtered.length/perPage). The pagination and info bar automatically reflect the filtered subset.

Click "JSX" to download. Manage page and perPage with useState. Use useMemo to compute the visible rows slice: const visible = useMemo(() => data.slice((page-1)*perPage, page*perPage), [page, perPage]). Use another useMemo for totalPages: Math.ceil(data.length/perPage). Derive the page number range in useMemo as well. Reset page to 1 in useEffect when perPage changes: useEffect(() => setPage(1), [perPage]).