testyuakagi
v1.0.19
Published
A lightweight, accessible vanilla JavaScript table with server-side pagination
Maintainers
Readme
Paginated Table
A tiny, zero-dependency JavaScript helper for server-side paginated tables.
Paginated Table gives plain HTML tables production-ready pagination, search, loading states, error handling, and accessibility — without shipping CSS, requiring a framework, or taking over your design system.
It is designed for server-rendered applications such as Django, Rails, Laravel, Phoenix, ASP.NET, or any backend that can return JSON.
Contents
- Why Paginated Table?
- Quick start
- Basic backend contract
- Default row rendering
- Custom row renderers
- Search
- POST requests and CSRF
- Styling
- Security model
- Accessibility
- Error handling
- Data attribute reference
- Full example
- Non-goals
- License
Quick start
1. Add a table
<input
id="user-search"
type="text"
class="form-control"
placeholder="Search users..."
>
<table
class="table table-hover align-middle"
data-role="paginated-table"
data-fetch-url="/api/users/"
data-fetch-method="GET"
data-per-page="10"
data-q-input-id="user-search"
data-page-button-class="btn btn-sm btn-outline-primary"
data-page-active-button-class="btn btn-sm btn-primary"
data-prev-button-class="btn btn-sm btn-outline-secondary"
data-next-button-class="btn btn-sm btn-outline-secondary"
data-prev-button-content="«"
data-next-button-content="»"
data-pagination-container-class="d-flex gap-1 justify-content-end"
>This example uses Bootstrap classes, but Bootstrap is not required. Any CSS class names work.
For all supported attributes, see the Data attribute reference.
<thead>
<tr>
<th data-header-key="id">ID</th>
<th data-header-key="name">Name</th>
<th data-header-key="email">Email</th>
</tr>
</thead>
</table>2. Import and initialize
import { initPaginatedTable } from "./paginated-table.js";
initPaginatedTable();If the script is loaded after the document is ready, Paginated Table can also auto-initialize tables with:
data-role="paginated-table"3. Return JSON from your backend
{
"data": [
{"id": 1, "name": "Alice", "email": "[email protected]"},
{"id": 2, "name": "Bob", "email": "[email protected]"}
],
"total_pages": 8
}The library sends these request parameters:
page=1
per_page=10
q=aliceFor GET, they are sent as query parameters.
For POST, they are sent as FormData.
For request-related options, see Request attributes.
Basic backend contract
Your endpoint should accept:
| Parameter | Description |
| ---------- | ------------------------ |
| page | 1-based page number |
| per_page | Number of rows requested |
| q | Optional search query |
Your endpoint should return:
| Field | Type | Description |
| ------------- | ----------------- | ------------------------- |
| data | array | Rows for the current page |
| total_pages | integer, optional | Total number of pages |
total_pages is recommended. If omitted, Paginated Table infers the last page when the returned row count is smaller than per_page.
Default row rendering
By default, rows are rendered using the table header keys.
<th data-header-key="name">Name</th>maps to:
{"name": "Alice"}If data-header-key is omitted, the header text is normalized:
<th>Full Name</th>becomes:
full_nameExample:
<table data-role="paginated-table" data-fetch-url="/api/users/">
<thead>
<tr>
<th data-header-key="id">ID</th>
<th data-header-key="name">Name</th>
<th data-header-key="created_at">Created</th>
</tr>
</thead>
</table>Response:
{
"data": [
{"id": 1, "name": "Alice", "created_at": "2026-04-28"}
],
"total_pages": 1
}Custom row renderers
Use a custom row renderer when you need links, badges, buttons, icons, or custom formatting.
import { rowRenderers } from "./paginated-table.js";
rowRenderers.userRow = (row) => {
const tr = document.createElement("tr");
const nameTd = document.createElement("td");
nameTd.textContent = row.name;
tr.appendChild(nameTd);
const statusTd = document.createElement("td");
const badge = document.createElement("span");
badge.className = row.is_active ? "badge text-bg-success" : "badge text-bg-secondary";
badge.textContent = row.is_active ? "Active" : "Inactive";
statusTd.appendChild(badge);
tr.appendChild(statusTd);
const actionTd = document.createElement("td");
const link = document.createElement("a");
link.className = "btn btn-sm btn-outline-primary";
link.href = `/users/${row.id}/`;
link.textContent = "Open";
actionTd.appendChild(link);
tr.appendChild(actionTd);
return tr;
};Then attach it to the table:
<table
data-role="paginated-table"
data-fetch-url="/api/users/"
data-row-renderer="userRow"
>
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
</table>A custom renderer receives one row object and must return a <tr> element.
For renderer-related options, see Rendering attributes.
Search
Add an input and connect it with data-q-input-id.
<input id="search" type="text" placeholder="Search...">
<table
data-role="paginated-table"
data-fetch-url="/api/items/"
data-q-input-id="search"
>
<thead>
<tr>
<th data-header-key="name">Name</th>
</tr>
</thead>
</table>Search is debounced and resets pagination to page 1.
See data-q-input-id for details.
POST requests and CSRF
For backends that require POST requests, configure the method and CSRF header.
Django example
<meta name="csrf-token" content="{{ csrf_token }}">
<table
data-role="paginated-table"
data-fetch-url="/api/users/"
data-fetch-method="POST"
data-csrf-token-header="X-CSRFToken"
>
<thead>
<tr>
<th data-header-key="id">ID</th>
<th data-header-key="name">Name</th>
</tr>
</thead>
</table>You may also provide the token directly:
<table
data-role="paginated-table"
data-fetch-url="/api/users/"
data-fetch-method="POST"
data-csrf-token="{{ csrf_token }}"
data-csrf-token-header="X-CSRFToken"
>See data-csrf-token and data-csrf-token-header for details.
Styling
Paginated Table ships no CSS.
This is intentional. The library creates semantic HTML and lets you use any styling system:
- Bootstrap
- Tailwind CSS
- Bulma
- Foundation
- Custom enterprise design systems
- Plain CSS
Example with Bootstrap-style classes:
<table
class="table table-striped"
data-role="paginated-table"
data-fetch-url="/api/orders/"
data-page-button-class="btn btn-sm btn-outline-primary"
data-page-active-button-class="btn btn-sm btn-primary"
data-prev-button-class="btn btn-sm btn-outline-secondary"
data-next-button-class="btn btn-sm btn-outline-secondary"
data-pagination-container-class="d-flex gap-1 justify-content-end"
>Security model
Paginated Table is safe by default for row data because the default renderer uses textContent.
Some configuration attributes intentionally support HTML. This is useful for icons, placeholders, and design-system markup.
These attributes are treated as trusted developer-provided HTML:
data-prev-button-contentdata-next-button-contentdata-ellipsis-button-contentdata-placeholder-html
In normal table usage, these values are usually static template values written by the developer. That is the intended use.
Safe:
<table
data-prev-button-content="«"
data-next-button-content="»"
data-placeholder-html="<span class='placeholder col-6'></span>"
>Avoid passing user-provided or database-provided HTML directly:
<!-- Avoid this unless the value is sanitized and trusted. -->
<table data-placeholder-html="{{ user_provided_html }}">If you need to render dynamic HTML, sanitize it first. A common option is DOMPurify.
const cleanHtml = DOMPurify.sanitize(untrustedHtml);For untrusted values, prefer textContent in a custom row renderer.
Accessibility
Paginated Table includes accessibility-aware behavior:
aria-busywhile loading- Screen-reader live region updates
aria-labelon pagination buttonsaria-current="page"for the active pagerole="navigation"on pagination controls- Search input role and label normalization
The library does not replace full accessibility testing, but it provides a strong baseline for production interfaces.
Error handling
If configuration or network errors occur, Paginated Table:
- Logs a detailed developer-facing error to the console.
- Replaces the table body with a user-facing error message.
Customize the error message and class:
<table
data-role="paginated-table"
data-fetch-url="/api/items/"
data-error-message="Could not load items. Please try again."
data-error-class="text-danger py-4 text-center"
>Data attribute reference
Required attributes
data-role
Marks the table for auto-initialization.
data-role="paginated-table"Required for automatic initialization.
data-fetch-url
The endpoint used to fetch table data.
data-fetch-url="/api/users/"The endpoint should return JSON with a data array and optionally total_pages.
Request attributes
data-fetch-method
HTTP method used for fetching data.
data-fetch-method="GET"Supported values:
GETPOST
Default:
GETdata-per-page
Number of rows requested per page.
data-per-page="25"Default:
10Allowed range:
1–500data-q-input-id
ID of the search input associated with the table.
<input id="user-search" type="text">
<table data-q-input-id="user-search">When the input changes, the table fetches page 1 with the current query.
data-csrf-token
CSRF token for POST requests.
data-csrf-token="{{ csrf_token }}"Optional if a supported meta tag exists:
<meta name="csrf-token" content="{{ csrf_token }}">data-csrf-token-header
Header name used to send the CSRF token.
data-csrf-token-header="X-CSRFToken"Common values:
| Framework | Header |
| ----------- | -------------- |
| Django | X-CSRFToken |
| Rails | X-CSRF-Token |
| Laravel | X-CSRF-TOKEN |
| Spring Boot | X-CSRF-TOKEN |
Required for POST requests.
Rendering attributes
data-row-renderer
Name of a custom row renderer registered in rowRenderers.
data-row-renderer="userRow"rowRenderers.userRow = (row) => {
const tr = document.createElement("tr");
return tr;
};If omitted, the default renderer uses header keys.
data-row-tr-class
Classes added to every rendered <tr>.
data-row-tr-class="align-middle"Multiple classes are supported:
data-row-tr-class="align-middle table-row-hover"data-no-record-message
Message shown when the first page has no records.
data-no-record-message="No users found."Default:
No datadata-no-record-td-class
Class added to the no-record <td>.
data-no-record-td-class="text-muted text-center py-4"data-pad-rows
Controls whether empty rows are added to reduce layout shift.
data-pad-rows="false"Default:
trueWhen enabled, the table keeps roughly the same height across pages.
data-placeholder-html
HTML shown while the table is initially loading.
data-placeholder-html="<span class='placeholder col-8'></span>"This value is treated as trusted developer-provided HTML. Do not pass unsanitized user input.
Pagination attributes
data-pagination-container-class
Class added to the pagination container.
data-pagination-container-class="d-flex gap-1 justify-content-end"data-page-button-class
Class added to normal page buttons.
data-page-button-class="btn btn-sm btn-outline-primary"data-page-active-button-class
Class added to the active page button.
data-page-active-button-class="btn btn-sm btn-primary"data-prev-button-class
Class added to the previous-page button.
data-prev-button-class="btn btn-sm btn-outline-secondary"data-next-button-class
Class added to the next-page button.
data-next-button-class="btn btn-sm btn-outline-secondary"data-prev-button-content
HTML content for the previous-page button.
data-prev-button-content="«"Default:
<<This value is treated as trusted developer-provided HTML.
data-next-button-content
HTML content for the next-page button.
data-next-button-content="»"Default:
>>This value is treated as trusted developer-provided HTML.
data-ellipsis-button-class
Class added to ellipsis buttons.
data-ellipsis-button-class="btn btn-sm btn-light disabled"data-ellipsis-button-content
HTML content for ellipsis buttons.
data-ellipsis-button-content="…"Default:
...This value is treated as trusted developer-provided HTML.
Error attributes
data-error-message
User-facing message shown when loading fails.
data-error-message="Failed to load data."Default:
Failed to load data.data-error-class
Class added to the error <td>.
data-error-class="text-danger text-center py-4"Debug attributes
data-verbose
Enables additional console messages during initialization.
data-verbose="true"Default:
falseInternal state attributes
Paginated Table manages these attributes internally:
data-initializeddata-current-pagedata-current-total-pagesdata-current-q
You usually should not set or modify them manually.
Full example
<input
id="order-search"
type="text"
class="form-control mb-3"
placeholder="Search orders..."
>
<table
class="table table-hover align-middle"
data-role="paginated-table"
data-fetch-url="/api/orders/"
data-fetch-method="GET"
data-per-page="20"
data-q-input-id="order-search"
data-row-renderer="orderRow"
data-row-tr-class="align-middle"
data-pad-rows="true"
data-placeholder-html="<span class='placeholder col-8'></span>"
data-no-record-message="No orders found."
data-no-record-td-class="text-muted text-center py-4"
data-page-button-class="btn btn-sm btn-outline-primary"
data-page-active-button-class="btn btn-sm btn-primary"
data-prev-button-class="btn btn-sm btn-outline-secondary"
data-next-button-class="btn btn-sm btn-outline-secondary"
data-prev-button-content="«"
data-next-button-content="»"
data-ellipsis-button-class="btn btn-sm btn-light"
data-ellipsis-button-content="…"
data-pagination-container-class="d-flex gap-1 justify-content-end"
data-error-message="Could not load orders. Please try again."
data-error-class="text-danger text-center py-4"
>
<thead>
<tr>
<th>Order</th>
<th>Customer</th>
<th>Status</th>
<th>Total</th>
<th></th>
</tr>
</thead>
</table>
<script type="module">
import { rowRenderers, initPaginatedTable } from "./paginated-table.js";
rowRenderers.orderRow = (row) => {
const tr = document.createElement("tr");
const orderTd = document.createElement("td");
orderTd.textContent = row.order_number;
tr.appendChild(orderTd);
const customerTd = document.createElement("td");
customerTd.textContent = row.customer_name;
tr.appendChild(customerTd);
const statusTd = document.createElement("td");
const badge = document.createElement("span");
badge.className = "badge text-bg-secondary";
badge.textContent = row.status;
statusTd.appendChild(badge);
tr.appendChild(statusTd);
const totalTd = document.createElement("td");
totalTd.textContent = row.total;
tr.appendChild(totalTd);
const actionTd = document.createElement("td");
const link = document.createElement("a");
link.className = "btn btn-sm btn-outline-primary";
link.href = `/orders/${row.id}/`;
link.textContent = "View";
actionTd.appendChild(link);
tr.appendChild(actionTd);
return tr;
};
initPaginatedTable();
</script>Non-goals
Paginated Table intentionally stays small. It does not include:
- Client-side sorting
- Column resizing
- Inline editing
- Virtual scrolling
- Drag and drop
- Built-in themes
- Built-in CSS
- Framework-specific adapters
These features are useful, but they add weight and complexity. Paginated Table focuses on server-side pagination, search, rendering, and accessibility.
License
MIT
