vanillajs-datatable is a powerful, feature-rich JavaScript library for creating interactive data tables. It provides a comprehensive set of features including pagination, sorting, filtering, searching, column visibility, row selection, and multiple export formats.
Built with modern JavaScript (ES6+), vanillajs-datatable is framework-agnostic and can be easily integrated into any web application. It supports multiple themes (Bootstrap, Tailwind CSS, DaisyUI) and provides a clean, intuitive API for developers.
vanillajs-datatable is built with:
vanillajs-datatable can be installed and used in several ways:
If you're using a bundler like Vite, Webpack, or Rollup:
npm install vanillajs-datatable
Then import and use it in your JavaScript:
import DataTable from "vanillajs-datatable";
const table = new DataTable({
// configuration options
});
For plain HTML files without a bundler, use the CDN version:
<script src="https://unpkg.com/vanillajs-datatable/dist/index.min.js"></script>
<script>
const table = new DataTable({
// configuration options
});
</script>
You can also download the files and include them directly in your project.
To get started with vanillajs-datatable, you need to:
vanillajs-datatable is highly configurable. First, create an HTML table element in your page:
<table class="datatable" id="usersDatatable">
</table>
Then, configure and initialize the DataTable with JavaScript:
const table = new DataTable({
tableId: 'usersDatatable', // ID of the table element (must match HTML)
url: '/api/users/datatable', // Backend API endpoint
dataSrc: 'data', // Key in response containing data array
columns: [
{ name: 'id', label: 'ID' },
{ name: 'name', label: 'Name' },
{ name: 'email', label: 'Email' }
],
});
Important: The tableId in your configuration must match the id
attribute of your HTML table element.
Your backend API should return a JSON response with the following structure:
{
"data": [
{ "id": 1, "name": "John Doe", "email": "john@example.com" },
{ "id": 2, "name": "Jane Smith", "email": "jane@example.com" }
// ... more rows
],
"total": 120,
"current_page": 1,
"last_page": 12
}
| Field | Type | Description |
|---|---|---|
data |
Array | Array of objects representing table rows |
total |
Number | Total number of records (all pages) |
current_page |
Number | Current page number |
last_page |
Number | Total number of pages (total / per_page) |
vanillajs-datatable supports powerful pagination controls to navigate large datasets efficiently.
Use the pagination flag to show or hide pagination controls:
const table = new DataTable({
tableId: "datatable",
pagination: true, // Show pagination buttons
// other options...
});
By default, detailed pagination with numbered page buttons is enabled.
If disabled (false), no pagination controls will be displayed and all data will be shown
on a single page.
Control the style of pagination using paginationType:
simple – Only "Previous" and "Next" buttons are shown.detailed – Numbered page buttons are displayed, allowing direct navigation
to any page.const table = new DataTable({
tableId: 'datatable',
pagination: true,
paginationType: 'detailed', // or 'simple'
// other options...
});
Pagination parameters (page and perPage) are sent automatically to your
API. When a user navigates to a different page, vanillajs-datatable sends a GET request to the API
with the updated page and perPage parameters.
Example request:
GET /api/users/datatable?page=2&perPage=10
Your backend should handle these parameters and return the appropriate page of data along with pagination metadata:
{
"data": [...], // Array of records for current page
"total": 120, // Total number of records
"current_page": 2, // Current page number
"last_page": 12 // Total number of pages
}
You can also control pagination programmatically using the Pagination API:
See the Pagination API section for detailed documentation on these methods.
vanillajs-datatable supports powerful sorting functionality that allows users to sort table data by clicking on column headers. Sorting can be enabled globally or per-column, and works seamlessly with server-side data.
Sorting is controlled globally using the sortable flag, and you can specify which
columns are sortable via the sortableColumns array:
const table = new DataTable({
tableId: "datatable",
sortable: true, // Enable sorting globally
sortableColumns: ["id", "name"], // Only these columns will support sorting
columns: [
{ name: "id", label: "ID" },
{ name: "name", label: "Name" },
{ name: "email", label: "Email" },
],
});
Note: If sortableColumns is not specified, all columns will be sortable when
sortable: true is set.
You can set a default sort column and direction when the table initializes:
const table = new DataTable({
tableId: "datatable",
sortable: true,
defaultSort: "name", // Column to sort by default
defaultOrder: "asc", // Sort direction: "asc" or "desc"
columns: [
{ name: "id", label: "ID" },
{ name: "name", label: "Name" },
{ name: "email", label: "Email" },
],
});
When sorting is enabled, vanillajs-datatable automatically sends sorting parameters to your backend API with each request.
sortBy — The column name being sorted (e.g., "name")order — The sort direction, either "asc" (ascending) or "desc" (descending)Example Request:
GET /users/datatable?sortBy=name&order=asc&page=1&perPage=10
Your API should:
sortBy and order parameters correctlyYou can also control sorting programmatically using the Sorting API:
See the Sorting API section for detailed documentation on these methods.
vanillajs-datatable provides powerful search functionality that allows users to search across table data in real-time. The search feature includes debouncing to optimize performance and works seamlessly with server-side data.
Enable a live search box at the top of your table by setting the searchable option to
true:
const table = new DataTable({
tableId: "datatable",
url: "/users/datatable",
columns: [
{ name: "id", label: "ID" },
{ name: "name", label: "Name" },
{ name: "email", label: "Email" }
],
searchable: true, // Enable the search box
});
The search input will appear automatically above the table and supports live debounced search.
You can control the debounce delay with the searchDelay option (in milliseconds):
const table = new DataTable({
tableId: "datatable",
url: "/users/datatable",
columns: [
{ name: "id", label: "ID" },
{ name: "name", label: "Name" },
{ name: "email", label: "Email" }
],
searchable: true,
searchDelay: 500, // Delay search requests by 500ms after typing
});
This helps reduce unnecessary server calls during fast typing. The default delay is 300ms.
?search=keywordWhen search is enabled, vanillajs-datatable automatically sends the search parameter to your backend API with each request.
search — The search keyword entered by the userExample Request:
GET /users/datatable?search=john&page=1&perPage=10&sortBy=name&order=asc
Your API should:
You can specify a custom search input element by providing the searchInputId option:
const table = new DataTable({
tableId: "datatable",
url: "/users/datatable",
searchable: true,
searchInputId: "my-custom-search", // Use custom input element
// other options...
});
If not specified, vanillajs-datatable will create a search input automatically above the table.
vanillajs-datatable provides flexible column-specific filtering with text inputs displayed above column headers. This feature allows users to filter data by individual columns, providing more precise control than global search.
Column filtering enables users to filter table data by specific columns using text inputs that appear above each column header. This feature can be toggled on/off and configured to show filters only for specific columns.
To enable column filtering, set these options:
const table = new DataTable({
tableId: "datatable",
url: "/users/datatable",
dataSrc: "users",
// Enable/disable the entire feature (default: false)
columnFiltering: true,
// Specify which columns get filters
filterableColumns: ["name", "email"],
columns: [
{ name: "id", label: "ID" },
{ name: "name", label: "Full Name" },
{ name: "email", label: "Email" },
{ name: "role", label: "Role" },
],
});
The column filtering system can be completely disabled while keeping the configuration:
const table = new DataTable({
// ...other config...
columnFiltering: false, // Entire feature disabled
filterableColumns: ["name", "email"], // Will be ignored
});
Only show filters for specific columns by listing them in filterableColumns:
filterableColumns: ["name", "email"]; // Only these columns get filters
If filterableColumns is not specified, no filters will be shown even if
columnFiltering: true.
searchDelay)?columnFilters[name]=value&columnFilters[email]=value
When column filtering is enabled, vanillajs-datatable automatically sends filter parameters to your backend API with each request.
Column filters are sent as an object in the query string:
columnFilters[name] — Filter value for the "name" columncolumnFilters[email] — Filter value for the "email" columnExample Request:
GET /users/datatable?columnFilters[name]=john&columnFilters[email]=example&page=1&perPage=10
Your API should:
You can also control filters programmatically using the Filter API:
See the Filter API section for detailed documentation on these methods.
The Column Visibility Toggle feature allows users to dynamically show/hide columns in the DataTable. This feature provides a user-friendly interface for customizing the table view based on individual preferences.
Add columnVisibility to your DataTable configuration:
const table = new DataTable({
tableId: "datatable",
url: "/users/datatable",
columns: columns,
// Column Visibility Configuration
columnVisibility: {
enabled: true, // Enable column visibility feature
showButton: true, // Show column visibility button in controls
persistState: true, // Save column visibility state (requires saveState: true)
},
// Optional: Enable state persistence to remember column visibility
saveState: true,
saveStateDuration: 60 * 60 * 1000, // 1 hour
});
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | false |
Enable/disable column visibility feature |
showButton |
boolean | true |
Show/hide the column visibility button in controls |
persistState |
boolean | true |
Save column visibility state to
localStorage (requires saveState: true) |
When enabled, a "Columns" button appears in the DataTable controls. Click it to open a dropdown menu with:
persistState: true and saveState: true, the state is saved to
localStorage
You can control which columns can be hidden by setting the visible property on
individual columns:
const columns = [
{ name: "id", label: "ID", visible: true }, // Can be hidden
{ name: "name", label: "Name", visible: true }, // Can be hidden
{ name: "actions", label: "Actions", visible: false }, // Cannot be hidden (required)
];
Note: Columns with visible: false are considered required and cannot be toggled
in the visibility menu.
See the Column Visibility API section for detailed documentation on these methods.
visible: false)vanillajs-datatable provides powerful row selection functionality that allows users to select single or multiple rows in the table. Selected rows can be styled, tracked, and used for batch operations, exports, or other actions.
Enable row selection using the selection configuration object:
const table = new DataTable({
tableId: "datatable",
url: "/api/users",
columns: [...],
selection: {
enabled: true,
mode: "multiple", // 'single'|'multiple'
selectedClass: "row-selected bg-blue-100", // Single property for all selection styling
},
});
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | false |
Enable or disable row selection functionality |
mode |
string | "single" |
Selection mode: "single" for
one row at a time, or "multiple" for multiple rows |
selectedClass |
string | "row-selected bg-blue-100"
|
CSS class(es) applied to selected rows. Can include multiple classes separated by spaces |
enabled: true, users can click on table rows to select them.mode: "single", clicking a row deselects any previously selected row and selects
the clicked row.mode: "multiple", users can select multiple rows by clicking them. Clicking a
selected row deselects it.selectedClass.
Allow users to select only one row at a time:
const table = new DataTable({
tableId: "datatable",
url: "/api/users",
columns: [...],
selection: {
enabled: true,
mode: "single",
selectedClass: "row-selected bg-blue-100",
},
});
In single selection mode:
Allow users to select multiple rows:
const table = new DataTable({
tableId: "datatable",
url: "/api/users",
columns: [...],
selection: {
enabled: true,
mode: "multiple",
selectedClass: "row-selected bg-blue-100",
},
});
In multiple selection mode:
The selectedClass property accepts a string of CSS classes that will be applied to
selected rows. You can include multiple classes:
const table = new DataTable({
tableId: "datatable",
url: "/api/users",
columns: [...],
selection: {
enabled: true,
mode: "multiple",
// Multiple classes for comprehensive styling
selectedClass: "row-selected bg-blue-100 border-l-4 border-blue-500",
},
});
Examples for different themes:
// Bootstrap theme
selection: {
enabled: true,
mode: "multiple",
selectedClass: "row-selected bg-success text-white",
},
// Tailwind theme
selection: {
enabled: true,
mode: "multiple",
selectedClass: "row-selected bg-indigo-100 border-l-4 border-indigo-500",
},
// DaisyUI theme
selection: {
enabled: true,
mode: "multiple",
selectedClass: "row-selected bg-primary text-primary-content",
},
Control selection programmatically using the Selection API:
See the Selection API section for detailed documentation on all selection methods.
mode: "single" for detail views, editing, or single-item operationsmode: "multiple" for batch operations, bulk exports, or multi-item actions
selectedClass styles that provide clear visual feedback without being
distracting
onSelectionChange callbacks to update UI elements (buttons, counters) based on
selection stateEnable infinite scrolling to load additional table data as the user scrolls down. This is useful for large datasets or streamlined UX where pagination is hidden. Infinite scroll provides a seamless user experience by automatically loading more data when the user approaches the bottom of the table.
Enable infinite scrolling in your DataTable configuration:
const table = new DataTable({
tableId: "datatable",
url: "/api/data",
columns: [...],
infiniteScroll: {
enabled: true, // Enable infinite scroll
scrollOffset: 10, // Trigger when user is 10px from bottom
hidePaginationOnScroll: true, // Hide pagination controls while scrolling
maxScrollPages: 100, // Stop after loading 100 pages
scrollWrapperHeight: "80vh", // Scrollable container height
},
});
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | false |
Enables or disables infinite scroll |
scrollOffset |
number | 10 |
Distance (in px) from bottom to trigger data load |
hidePaginationOnScroll |
boolean | false |
Hides pagination UI when infinite scroll is active |
maxScrollPages |
number | 100 |
Maximum number of pages to auto-load |
scrollWrapperHeight |
string | "auto" |
Height of the scroll container (e.g., "75vh", "500px", etc.) |
scrollOffset pixels of the bottom, the next page is
automatically loadedmaxScrollPages is reachedhidePaginationOnScroll is enabled, pagination controls are hidden automatically
The scrollOffset determines how close to the bottom the user must scroll before
triggering the next page load:
infiniteScroll: {
enabled: true,
scrollOffset: 20, // Trigger 20px before reaching bottom
}
Control the height of the scrollable container using scrollWrapperHeight:
infiniteScroll: {
enabled: true,
scrollWrapperHeight: "80vh", // 80% of viewport height
// Other options:
// scrollWrapperHeight: "500px", // Fixed pixel height
// scrollWrapperHeight: "auto", // Auto height (default)
}
Note: The height must include a unit (vh, px, etc.). Using "auto" allows the container to grow naturally.
Infinite scroll works best with server-side data loading. The system automatically sends pagination parameters to your API:
GET /api/data?page=1&perPage=10
GET /api/data?page=2&perPage=10
GET /api/data?page=3&perPage=10
// ... continues until maxScrollPages is reached
Your backend should return data in the standard format:
{
"data": [...], // Array of records for current page
"total": 1000, // Total number of records
"current_page": 2, // Current page number
"last_page": 100 // Total number of pages
}
scrollOffset values (10-20px recommended)maxScrollPages to prevent excessive data loadinghidePaginationOnScroll for cleaner UI when using infinite scrollmaxScrollPages is reachedscrollOffset for faster triggering, or increase it to reduce
sensitivity
hidePaginationOnScroll is enabled, the pagination UI is hidden automatically
while scrollinghidePaginationOnScroll: true
const table = new DataTable({
tableId: "datatable",
url: "/api/users",
dataSrc: "users",
columns: [
{ name: "id", label: "ID" },
{ name: "name", label: "Name" },
{ name: "email", label: "Email" },
],
infiniteScroll: {
enabled: true,
scrollOffset: 10,
hidePaginationOnScroll: true,
maxScrollPages: 1000,
scrollWrapperHeight: "80vh",
},
// Other options...
perPage: 10,
pagination: true, // Can be hidden when infinite scroll is active
});
Save and restore table state including sorting, pagination, filters, and column visibility. State is persisted in localStorage.
vanillajs-datatable provides flexible control over the number of rows displayed per page. You can configure the default page size, provide options for users to change it, and control whether the per-page selector dropdown is displayed.
Configure items per page using three main properties:
const table = new DataTable({
tableId: "datatable",
url: "/api/users",
columns: [...],
perPage: 100, // Default rows per page
perPageSelector: true, // Show per page selector dropdown
perPageOptions: [25, 100, 200, 250], // Options in dropdown
});
| Option | Type | Default | Description |
|---|---|---|---|
perPage |
number | 10 |
Default number of rows displayed per page |
perPageSelector |
boolean | true |
Show/hide the per-page selector dropdown in the table controls |
perPageOptions |
array | [10, 25, 50] |
Array of numbers representing available options in the dropdown |
perPageSelectId |
string | null |
Custom ID for the per-page selector element (auto-generated if not provided) |
perPage value determines how many rows are initially displayed and sent to the
server in API requests.perPageSelector: true, a dropdown appears in the table controls allowing users
to change the page size.perPageOptions array defines the available choices in the dropdown.perPage value.
?perPage=100
perPageSelector: false, users cannot change the page size, and the table uses
the perPage value you specified.Set the initial number of rows per page using perPage:
const table = new DataTable({
tableId: "datatable",
url: "/api/users",
columns: [...],
perPage: 50, // Show 50 rows per page by default
});
Note: The perPage value should ideally be one of the values in your
perPageOptions array to ensure consistency, though it's not strictly required.
Define the available options users can choose from:
const table = new DataTable({
tableId: "datatable",
url: "/api/users",
columns: [...],
perPage: 25,
perPageOptions: [10, 25, 50, 100, 200], // Custom options
perPageSelector: true,
});
You can use any numbers you want, and they don't need to be in ascending order (though it's recommended for better UX):
const table = new DataTable({
tableId: "datatable",
url: "/api/users",
columns: [...],
perPage: 100,
perPageOptions: [25, 100, 250, 500], // Custom order and values
perPageSelector: true,
});
If you want to use a fixed page size without allowing users to change it:
const table = new DataTable({
tableId: "datatable",
url: "/api/users",
columns: [...],
perPage: 50,
perPageSelector: false, // Hide the selector dropdown
// perPageOptions is ignored when perPageSelector is false
});
When users change the page size, vanillajs-datatable automatically sends the new perPage
value to your backend API:
Example Request:
GET /api/users/datatable?page=1&perPage=100
Your backend should:
perPage query parameterperPage value{
"data": [...], // Array of records (up to perPage items)
"total": 500, // Total number of records
"current_page": 1, // Current page number
"last_page": 5, // Total pages (calculated from total / perPage)
}
You can change the page size programmatically using the Pagination API:
See the Pagination API section for more details on programmatic pagination control.
perPage value that balances performance and usability (10-100 is typical)
perPage value in your perPageOptions array for
consistency
perPageOptions in ascending order for better user experienceperPage values (e.g., 500+)
perPageOptions (e.g., [10, 25, 50, 100] rather than
[10, 11, 12, 13])perPage value
perPage values to ensure your API handles them correctlyperPage to prevent performance
issues
vanillajs-datatable provides a comprehensive API for interacting with the table programmatically. All API methods are organized by category.
vanillajs-datatable provides a comprehensive Event Handler API that allows you to respond to user interactions with table rows and cells. You can register callbacks for row clicks, cell clicks, and row hover events to create interactive and responsive table experiences.
The Event Handler API consists of six main methods:
onRowClick(), onCellClick(),
onRowHover()
removeRowClickHandlers(),
removeCellClickHandlers(), removeRowHoverHandlers()
Register a callback function that will be called when a user clicks on a table row.
| Parameter | Type | Description |
|---|---|---|
callback |
Function | Callback function with signature:
(rowId, rowData, rowElement, event)
|
rowId (string|number) - The ID of the clicked rowrowData (Object) - Complete row data object containing all column valuesrowElement (HTMLElement) - The DOM element of the clicked rowevent (Event) - Native click event object (allows checking modifier keys)table.onRowClick((rowId, rowData, rowElement, event) => {
console.log("Row clicked:", rowId);
console.log("Row data:", rowData);
// Navigate to detail page
window.location.href = `/users/${rowId}`;
});
You can register multiple callbacks - all will be executed in registration order:
// First callback - navigation
table.onRowClick((rowId, rowData, rowElement, event) => {
window.location.href = `/users/${rowId}`;
});
// Second callback - analytics tracking
table.onRowClick((rowId, rowData, rowElement, event) => {
analytics.track("row_clicked", { rowId, rowData });
});
// Third callback - visual feedback
table.onRowClick((rowId, rowData, rowElement, event) => {
rowElement.style.transition = "background-color 0.2s";
rowElement.style.backgroundColor = "#e3f2fd";
setTimeout(() => {
rowElement.style.backgroundColor = "";
}, 300);
});
role="button" are automatically ignored
Register a callback function that will be called when a user clicks on a table cell.
| Parameter | Type | Description |
|---|---|---|
callback |
Function | Callback function with signature:
(rowId, columnName, cellValue, cellElement, rowElement, rowData, event)
|
rowId (string|number) - The ID of the row containing the clicked cellcolumnName (string) - The name of the clicked columncellValue (string) - The text content of the clicked cellcellElement (HTMLElement) - The DOM element of the clicked cellrowElement (HTMLElement) - The DOM element of the rowrowData (Object) - Complete row data objectevent (Event) - Native click event objecttable.onCellClick((rowId, columnName, cellValue, cellElement, rowElement, rowData, event) => {
console.log("Cell clicked:", columnName, "=", cellValue);
// Handle different columns differently
if (columnName === "email") {
window.location.href = `mailto:${cellValue}`;
} else if (columnName === "phone") {
window.location.href = `tel:${cellValue}`;
}
});
table.onCellClick((rowId, columnName, cellValue, cellElement, rowElement, rowData, event) => {
switch (columnName) {
case "email":
window.location.href = `mailto:${cellValue}`;
break;
case "phone":
window.location.href = `tel:${cellValue}`;
break;
case "website":
window.open(cellValue, "_blank");
break;
case "name":
// Double-click to edit
if (event.detail === 2) {
editCellInline(cellElement, rowId, columnName, cellValue);
}
break;
default:
// Copy to clipboard for other cells
navigator.clipboard.writeText(cellValue);
showToast("Copied to clipboard");
}
});
table.onCellClick((rowId, columnName, cellValue, cellElement, rowElement, rowData, event) => {
// Highlight clicked cell
cellElement.style.backgroundColor = "#fff3cd";
cellElement.style.transition = "background-color 0.3s";
setTimeout(() => {
cellElement.style.backgroundColor = "";
}, 800);
// Copy to clipboard
navigator.clipboard.writeText(cellValue);
console.log("Copied to clipboard:", cellValue);
});
Register a callback function that will be called when a user hovers over a table row (mouseenter event).
| Parameter | Type | Description |
|---|---|---|
callback |
Function | Callback function with signature:
(rowId, rowData, rowElement, event)
|
rowId (string|number) - The ID of the hovered rowrowData (Object) - Complete row data objectrowElement (HTMLElement) - The DOM element of the hovered rowevent (Event) - Native mouseenter event objecttable.onRowHover((rowId, rowData, rowElement, event) => {
console.log("Row hovered:", rowId);
// Show tooltip or preview
showRowPreview(rowData);
// Add visual feedback
rowElement.style.cursor = "pointer";
rowElement.style.transition = "background-color 0.2s";
});
let tooltip = null;
table.onRowHover((rowId, rowData, rowElement, event) => {
// Create or update tooltip
if (!tooltip) {
tooltip = document.createElement("div");
tooltip.className = "tooltip";
document.body.appendChild(tooltip);
}
tooltip.textContent = `ID: ${rowId} | Name: ${rowData.name} | Email: ${rowData.email}`;
tooltip.style.display = "block";
tooltip.style.left = event.pageX + 10 + "px";
tooltip.style.top = event.pageY + 10 + "px";
});
// Hide tooltip on mouseleave
table.table.addEventListener("mouseleave", () => {
if (tooltip) {
tooltip.style.display = "none";
}
});
Remove all registered callbacks for specific event types. This is useful for cleanup or dynamic handler management.
removeRowClickHandlers() - Remove all row click callbacksremoveCellClickHandlers() - Remove all cell click callbacksremoveRowHoverHandlers() - Remove all row hover callbacks// Remove all row click handlers
table.removeRowClickHandlers();
// Remove all cell click handlers
table.removeCellClickHandlers();
// Remove all row hover handlers
table.removeRowHoverHandlers();
// Register handlers
table.onRowClick(handleRowClick);
table.onCellClick(handleCellClick);
table.onRowHover(handleRowHover);
// Later, remove all handlers when component unmounts
function cleanup() {
table.removeRowClickHandlers();
table.removeCellClickHandlers();
table.removeRowHoverHandlers();
}
Here's a complete example using all event handler methods:
const table = new DataTable({
tableId: "datatable",
url: "/api/users",
columns: [
{ name: "id", label: "ID" },
{ name: "name", label: "Name" },
{ name: "email", label: "Email" },
{ name: "phone", label: "Phone" },
],
});
// Row click handler - navigate to detail page
table.onRowClick((rowId, rowData, rowElement, event) => {
// Check for modifier keys
if (event.ctrlKey || event.metaKey) {
// Open in new tab
window.open(`/users/${rowId}`, "_blank");
} else {
// Navigate in same window
window.location.href = `/users/${rowId}`;
}
});
// Cell click handler - handle different columns
table.onCellClick((rowId, columnName, cellValue, cellElement, rowElement, rowData, event) => {
switch (columnName) {
case "email":
window.location.href = `mailto:${cellValue}`;
break;
case "phone":
window.location.href = `tel:${cellValue}`;
break;
case "name":
// Double-click to edit
if (event.detail === 2) {
editUser(rowId);
}
break;
default:
// Copy to clipboard
navigator.clipboard.writeText(cellValue);
showNotification("Copied to clipboard");
}
});
// Row hover handler - show preview
table.onRowHover((rowId, rowData, rowElement, event) => {
showUserPreview(rowData);
rowElement.style.cursor = "pointer";
});
// Cleanup on page unload
window.addEventListener("beforeunload", () => {
table.removeRowClickHandlers();
table.removeCellClickHandlers();
table.removeRowHoverHandlers();
});
Event handlers work seamlessly with other DataTable features:
The Column Visibility API lets you programmatically show, hide, and inspect columns in your table. It
works hand-in-hand with the column visibility toggle button and dropdown (when
columnVisibility.enabled is true) so you can build complex workflows such as hide/show
presets, bulk operations, and state persistence.
getVisibleColumns() – Returns all currently visible column objectsisColumnVisible(columnName) – Checks whether the named column is visibletoggleColumnVisibility(columnName, visible?) – Toggle or force visibilityshowColumn(columnName) / hideColumn(columnName) – Convenience wrappers
showAllColumns() / hideAllColumns() – Bulk show/hide (required columns
stay visible)resetColumnVisibility() – Restore each column's initial visibility statebindColumnVisibilityButton() – Internal helper for wiring the controls dropdown
const table = new DataTable({
tableId: "datatable",
url: "/api/users",
columnVisibility: {
enabled: true,
showButton: true,
persistState: true,
},
});
Once column visibility is enabled, use the API:
table.toggleColumnVisibility("email"); // Toggle email column
table.showColumn("phone"); // Force show
table.hideColumn("internal_id"); // Hide optional column
const visibleColumns = table.getVisibleColumns();
console.log("Visible columns:", visibleColumns.map((col) => col.name));
getVisibleColumns()visible: false and hidden state
from user toggles).isColumnVisible(columnName)column.visible metadata.toggleColumnVisibility(columnName, visible?)true/false to force a state.
Required columns cannot be hidden.showAllColumns() / hideAllColumns()required: true columns.resetColumnVisibility()visible setting (true unless you explicitly set
visible: false).
bindColumnVisibilityButton()columnVisibility.showButton.
// Show all optional columns
table.showAllColumns();
// Hide everything except required columns
table.hideAllColumns();
// Reset user visibility preferences
table.resetColumnVisibility();
// Toggle a specific column with a button
document.getElementById("hide-email").addEventListener("click", () => {
table.hideColumn("email");
});
When you enable columnVisibility.showButton, the API automatically binds a dropdown that
lists each column with a checkbox. It also adds
Show All, Hide All, and Reset buttonspersistState: true and saveState: truerequired: true so users can’t hide them.toggleColumnVisibility to build presets (e.g., compact vs. detailed views).
showAllColumns() before exporting so hidden columns are included.visible columns if you’re serializing view state.See the Column Visibility Guide for detailed control examples.
The Pagination API gives you full programmatic control over navigation and page size so that you can hook the table into custom controls, wizard flows, or dashboard widgets.
goToPage(pageNumber) – Jump to a specific pagesetPageSize(size) – Update the number of rows shown per pagegetCurrentPage() – Read the current page indexnextPage() / prevPage() – Move forward or backwardfirstPage() / lastPage() – Jump to the very start or end| Method | Parameters | Description |
|---|---|---|
goToPage(pageNumber) |
number | Navigates to a specific page if the number is valid |
setPageSize(size) |
number | Changes the rows-per-page value and reloads data |
getCurrentPage() |
— | Returns the current page index (1-based) |
nextPage() |
— | Moves to the next page (if one exists) |
prevPage() |
— | Moves to the previous page (if one exists) |
firstPage() |
— | Jumps to page 1 |
lastPage() |
— | Jumps to the last page if
totalPages is known
|
const nextBtn = document.getElementById("custom-next");
const prevBtn = document.getElementById("custom-prev");
const sizeSelect = document.getElementById("page-size");
nextBtn.addEventListener("click", () => {
table.nextPage();
});
prevBtn.addEventListener("click", () => {
table.prevPage();
});
sizeSelect.addEventListener("change", (event) => {
table.setPageSize(Number(event.target.value));
});
// Jump to page 3
table.goToPage(3);
goToPage() to avoid warningssetPageSize()firstPage() and lastPage() when building keyboard shortcuts or
jump linkssaveState: true to persist the user-selected page size and page number
goToPage(1) after running a search to avoid empty
pages
goToPage(1) if you
want to reset viewThe Selection API exposes every tool you need to read, mutate, and react to the selectable rows
feature. It works on top of the selection config options and integrates with selection
callbacks, analytics, and batch workflows.
getSelectedIds() – Return IDs of currently selected rowsgetSelectedRows() / getSelectedData() – Access DOM elements or data
getSelectedCount() – Total selected rows (useful for badges)clearSelection() / selectAll() – Reset or bulk selecttoggleRowSelection(id, force) – Select/deselect a single row programmaticallyisSelected(id) – Check whether a row is already selectedselectRange(from, to) – Select a contiguous range by IDsetSelection(ids) – Replace selection with a custom setinvertSelection() – Flip every row’s selection statesetSelectable(flag) / setSelectMode(mode) – Toggle selection APIs
on/off
destroySelectable() – Remove selection behavior entirely| Method | Parameters | Description |
|---|---|---|
getSelectedIds() |
— | Returns an array of selected row IDs |
getSelectedData() |
— | Returns the original row data objects |
getSelectedRows() |
— | Returns the DOM row elements currently selected |
getSelectedCount() |
— | Count of selected rows |
clearSelection() |
— | Deselects every row |
selectAll() |
— | Selects every selectable row (respects
required)
|
toggleRowSelection(id, force?)
|
string|number, boolean | Toggle or force a single row’s selection |
isSelected(id) |
string|number | Returns whether the row is currently selected |
selectRange(from, to) |
string|number, string|number | Selects all rows between the two IDs (inclusive) |
setSelection(ids) |
Array | Replace the current selection with a whitelist of IDs |
invertSelection() |
— | Selects unselected rows and deselects selected ones |
setSelectable(flag) |
boolean | Enable/disable the selection feature dynamically |
setSelectMode(mode) |
string | Switch between single and
multiple selection modes
|
destroySelectable() |
— | Completely tears down the selection plugin |
const table = new DataTable({
tableId: "datatable",
url: "/api/users",
selection: {
enabled: true,
mode: "multiple",
},
});
// Watch selection changes
table.onSelectionChange((selectedIds) => {
document.getElementById("selected-count").textContent = selectedIds.length;
});
// Programmatically select a range for bulk actions
table.selectRange(10, 20);
// Force select a row
table.toggleRowSelection(5, true);
// Deselect all before exporting
table.clearSelection();
// Select all before a bulk update
table.selectAll();
clearSelection() after completing a batch action to reset UI stateselectRange() or setSelection() for keyboard-friendly multi-select
shortcuts
onSelectionChange() to enable/disable bulk action buttonsselection.enabled can be toggled at
runtime
destroySelectable() before reinitializing the table to avoid duplicate
handlers
The Data API exposes the read/write operations that let you manage the rows inside the DataTable.\n+ Use them to log the current dataset, mutate rows, perform searches, and manually redraw after bulk\n+ changes.
getData() – Returns the current dataset (after filters/sorts)getRowData(rowId) / getRowIndex(rowId) – Read a single rowgetRowsBy(field, value) / findRowsByFieldContains(field, value) –
Field-based lookupsaddRow(rowData, silent?) / addRows(rowsData, silent?) – Append data
updateRow(rowId, updates, silent?) / updateRows(rows, silent?) –
Modify rowsdeleteRow(rowId) / deleteRows(ids) – Remove rows by IDredraw() / draw() – Re-render table without refetchingThese methods are handy inside buttons, exports, or async callbacks:
// Log current data (includes sorting/filtering)
console.log(table.getData());
// Read a specific record
const rowData = table.getRowData(5);
// Add a new row and trigger render
table.addRow({ id: 11, name: 'New', email: 'new@example.com' });
// Update a row without fetching
table.updateRow(3, { name: 'Updated User' });
// Delete rows
table.deleteRows([2, 4]);
// Find rows by partial match
table.findRowsByFieldContains('name', 'smith');
// Redraw the table after batching multiple calls
table.redraw();
getData()getRowData(rowId)id.getRowIndex(rowId)getRowsBy(field, value)row[field] === value.findRowsByFieldContains(field, value)addRow(rowData, silent?)true for silent to skip redrawing immediately.
addRows(rowsData, silent?)silent and a final redraw().updateRow(rowId, updates, silent?)updateRows(rows, silent?)deleteRow(rowId)deleteRows(ids)redraw() / draw()document.getElementById('btn-log-data').addEventListener('click', () => {
console.log('Full data:', table.getData());
});
document.getElementById('btn-find').addEventListener('click', () => {
const matches = table.findRowsByFieldContains('name', 'oda');
console.log('Matches:', matches);
});
// Bulk updates
const updates = [
{ id: 1, updates: { email: 'safe@example.com' } },
{ id: 2, updates: { status: 'live' } },
];
table.updateRows(updates, true); // silent update
// Redraw once everything is ready
table.redraw();
id field when adding rows.silent updates plus a single redraw() to avoid flickering.getSelectedData() before deleting rows to confirm what will be removed.findRowsByFieldContains with custom filters for search helpers.The Filter API lets you read and mutate the column filters without touching the DOM inputs directly. Use it to build custom filter controls, clear filters programmatically, or apply defaults on load.
setFilter(key, value, silent?) – Set a filter value, with optional silent moderemoveFilter(key) – Remove a specific filterclearFilters() – Reset all filters (useful when resetting state)// Apply a filter without immediately fetching (silent)
table.setFilter("status", "active", true);
// Remove a single filter
table.removeFilter("status");
// Clear all filters
table.clearFilters();
setFilter writes to context.filters; if silent is false
it refetches the dataremoveFilter deletes a key so the value no longer travels to the backendclearFilters wipes the entire filters map; typically followed by
table.goToPage(1)
Combine filter calls with the Pagination API for reactive UIs:
table.setFilter("category", "sales");
table.goToPage(1); // reset to first page after filtering
silent: true when you plan to batch several filters before reloadingclearFilters() as part of your global reset buttonsaveState: true if you want users to return to the same
view
saveState: true is
enabled
Use the Sorting API to programmatically apply or clear sort orders. These helpers respect your configured sortable columns and trigger data reloads automatically.
setSort(column, direction = "asc") – Sort by a column and directionclearSort() – Remove the current sort order (resets to defaults)// Sort by name descending
table.setSort("name", "desc");
// Clear sorting
table.clearSort();
setSort updates context.sort and context.order, then
fetches new dataclearSort restores the default sort column/order and refetches datagoToPage(1) after applying a new sort to avoid empty pagesUtility helpers provide small but useful actions that touch the browser APIs directly. Currently the
main\n+ helper is copyToClipboard(), which copies the current table data (current page
plus filters)\n+ to the clipboard
in either CSV or TSV format.
Copies the currently rendered table rows (respecting sorting, filtering, and column visibility) to the\n+ system clipboard.
| Parameter | Type | Description |
|---|---|---|
format |
string | "csv" (default) or "tsv" to toggle delimiters |
column.visible and requested
visibility state)navigator.clipboard and logs success/failuredocument.getElementById("copy-csv").addEventListener("click", () => {
table.copyToClipboard("csv");
});
document.getElementById("copy-tsv").addEventListener("click", () => {
table.copyToClipboard("tsv");
});
copyToClipboard("tsv") when pasting into spreadsheets that expect tab
delimiters
vanillajs-datatable provides built-in export options that let users download the table data in various formats or print it directly from the UI.
Use the exportable config block inside your DataTable setup to fully control export
functionality:
const table = new DataTable({
tableId: "datatable",
url: "/users/datatable",
dataSrc: "users",
columns: columns,
exportable: {
enabled: true, // Master toggle for export
buttons: {
print: true,
excel: true,
csv: true,
pdf: true,
},
fileName: {
print: "leads_report",
pdf: "leads_pdf",
excel: "leads_excel",
csv: "leads_csv",
},
chunkSize: {
print: 1000,
pdf: 500,
excel: 500,
csv: 500,
},
pdfOptions: {
orientation: "portrait", // or "landscape"
unit: "mm", // units: "pt", "mm", "cm", "px"
format: "a4", // page size
theme: "grid", // table style: "striped", "grid"
},
},
});
When exportable.enabled is true, the following buttons can be individually
toggled:
| Button | Key | Description |
|---|---|---|
print |
Opens print preview window | |
| Excel | excel |
Downloads .xlsx file |
| CSV | csv |
Downloads .csv file |
pdf |
Downloads a styled PDF document |
In server-side mode (when not all data is loaded at once), the export system fetches data in chunks to avoid browser overload or server timeout:
chunkSize: {
print: 1000,
pdf: 500,
excel: 500,
csv: 500,
}
This will make repeated API calls like:
GET /users/datatable?export=true&page=1&pageSize=500
GET /users/datatable?export=true&page=2&pageSize=500
GET /users/datatable?export=true&page=3&pageSize=500
// ... until all rows are loaded
All chunks are then combined and exported as a single file.
Export table data to PDF with customizable styling, headers, footers, and custom elements.
Customize the appearance and layout of the PDF export:
pdfOptions: {
orientation: 'portrait', // or 'landscape'
unit: 'mm', // 'pt', 'mm', 'cm', 'px'
format: 'a4', // 'a4', 'letter', 'legal', etc.
theme: 'grid', // 'striped', 'grid', 'plain'
headerStyles: {
fillColor: [41, 128, 185], // RGB color array [R, G, B]
textColor: 255, // Text color (0-255, where 255 = white)
},
}
Export to Excel format (.xlsx) with formatting and styling options. Excel exports preserve column formatting and support large datasets through chunking.
Export to CSV format with proper escaping and encoding. CSV exports are lightweight and compatible with spreadsheet applications.
Export data as JSON. Supports exporting:
Print-friendly export with customizable headers and footers. Opens a print preview window that users can print directly from their browser.
Customize exports with custom elements, images, watermarks, and styling. See the Export Customization Guide for detailed examples.
Track export progress with progress indicators and callbacks. See the Export Progress Guide for detailed documentation.
The export customization module lets you decorate PDF and Print outputs with extra text or images.
Define customElements inside the exportable block and target each format
(PDF, Print, etc.).
type: "text" or "image"text/content: The string to render (PDF only)image: URL, File, or base64 for logosposition: e.g., "top-right", "center", "bottom-left" (or "custom" with explicit
coordinates)
fontSize, fontStyle, color, opacityrepeatOnPages: When true, renders on every pageexportable: {
enabled: true,
customElements: {
pdf: [
{
type: "text",
text: "Company Confidential",
position: "top-right",
fontSize: 10,
opacity: 0.7,
},
{
type: "image",
image: "/logo.png",
position: "top-left",
width: 45,
height: 45,
},
{
type: "text",
text: "DRAFT",
position: "center",
fontSize: 40,
color: [200, 200, 200],
opacity: 0.1,
angle: 45,
repeatOnPages: true,
},
],
print: [
{
type: "text",
text: "Printed from vanillajs-datatable",
position: "bottom-center",
fontSize: 8,
},
],
},
}
calculatePosition() converts human-friendly positions into x/y coordinates.applyTextToPdf()/applyImageToPdf() draw content via jsPDF.repeatOnPages for headers/footers.The export progress UI shows a modal and floating toggle button while exports are in-flight. These helpers are tied to the export pipeline so you can customize the UX.
showExportProgress(type, total): Opens the modal with a cancel button.updateExportProgress(current, total): Refreshes the bar/text (called internally).
hideExportProgress(): Closes the modal and toggle button after export finishes.
hideProgressModal()/showProgressModal(): Temporarily toggle the modal.
cancelExport(): Aborts the running export (uses AbortController).table.showExportProgress("csv", 5000);
table.on("exportProgress", (current, total, type) => {
table.updateExportProgress(current, total);
});
table.on("exportComplete", () => {
table.hideExportProgress();
});
document.getElementById("cancel-export").addEventListener("click", () => {
table.cancelExport();
});
exportProgressOverlay and
exportProgressBar.
exportProgress.isActive to false and aborts requests.
exportable.enabled = trueexportable: false are excluded from all exportsvanillajs-datatable supports multiple pre-built themes to fit your UI framework or style preferences. You can easily switch between themes like DaisyUI, Tailwind CSS, and Bootstrap, or customize styles with your own overrides.
Below are the built-in themes you can choose as the foundation for your DataTable styles:
| Theme Name | Description |
|---|---|
daisyui |
Uses DaisyUI classes for a Tailwind-based UI with DaisyUI components and utilities. |
tailwind |
Minimal Tailwind CSS utilities without any component library dependencies. Includes dark mode support. |
bootstrap |
Uses Bootstrap 5 classes for styling tables, buttons, and controls. Supports Bootstrap's dark mode. |
You can select the base theme via the baseTheme parameter when initializing the
DataTable. Additionally, you can override specific class names using the theme object.
const table = new DataTable({
tableId: "datatable",
url: "/users/datatable",
columns: columns,
baseTheme: "daisyui",
// Optional overrides
theme: {
row: "hover:bg-base-300",
button: "btn btn-primary btn-sm",
},
});
const table = new DataTable({
tableId: "datatable",
url: "/users/datatable",
columns: columns,
baseTheme: "bootstrap",
theme: {
button: "btn btn-primary btn-sm",
table: "table table-striped table-hover",
},
});
const table = new DataTable({
tableId: "datatable",
url: "/users/datatable",
columns: columns,
baseTheme: "tailwind",
theme: {
paginationButtonActive: "bg-blue-600 text-white",
row: "hover:bg-gray-100",
},
});
You can override any of the following theme keys to customize the appearance of your DataTable. Each key corresponds to a specific part of the table UI:
| Theme Key | Description |
|---|---|
controlsContainer |
Container for all table controls (search, buttons, etc.) |
controlsWrapper |
Wrapper for control elements |
controlsLeft |
Left side of controls container |
buttonGroup |
Group of action buttons |
perPageSelect |
Per page selector dropdown |
searchWrapper |
Search input wrapper |
searchIcon |
Search icon styling |
searchInput |
Search input field |
button |
General button styling |
table |
Main table element |
header |
Table header row |
headerCell |
Individual header cell |
headerSticky |
Sticky header styling (when stickyHeader is enabled) |
groupHeaderRow |
Column group header row |
groupHeaderCell |
Column group header cell |
filterRow |
Column filter row |
filterInput |
Column filter input field |
body |
Table body element |
row |
Table row styling |
cell |
Table cell styling |
highlight |
Search result highlighting |
paginationContainer |
Pagination container |
paginationInfo |
Pagination info text |
paginationWrapper |
Pagination buttons wrapper |
paginationButton |
Pagination button |
paginationButtonActive |
Active pagination button |
paginationButtonDisabled
|
Disabled pagination button |
paginationEllipsis |
Pagination ellipsis indicator |
advancedFilterToggle |
Advanced filter toggle button |
advancedFilterArrow |
Advanced filter arrow icon |
advancedFilterRow |
Advanced filter row container |
advancedFilterDiv |
Advanced filter form control container |
advancedFilterLabel |
Advanced filter label |
advancedFilterInputs |
Advanced filter inputs container |
advancedFilterInput |
Advanced filter input field |
advancedFilterButtonContainer
|
Advanced filter button container |
advancedFilterButton |
Advanced filter apply button |
scrollWrapperClass |
Infinite scroll wrapper |
scrollLoaderClass |
Infinite scroll loader text |
editableInput |
Editable cell input field |
editableSelect |
Editable cell select dropdown |
borderSuccess |
Success border class (for editable cells) |
borderError |
Error border class (for editable cells) |
borderLoading |
Loading border class (for editable cells) |
columnVisibilityDropdown
|
Column visibility dropdown menu |
columnVisibilityList |
Column visibility list container |
columnVisibilityItem |
Column visibility list item |
columnVisibilityCheckbox
|
Column visibility checkbox |
columnVisibilityLabel |
Column visibility label |
columnVisibilityLabelRequired
|
Required column visibility label |
columnVisibilityActions |
Column visibility action buttons container |
columnVisibilityActionButton
|
Column visibility action button |
exportProgressOverlay |
Export progress modal overlay |
exportProgressModal |
Export progress modal container |
exportProgressTitle |
Export progress modal title |
exportProgressBarContainer
|
Export progress bar container |
exportProgressBarFill |
Export progress bar fill |
exportProgressText |
Export progress text |
exportProgressTime |
Export progress time estimate |
exportProgressNote |
Export progress note |
exportProgressCancel |
Export progress cancel button |
Override specific theme keys to customize the appearance:
const table = new DataTable({
tableId: "datatable",
url: "/users/datatable",
columns: columns,
baseTheme: "tailwind",
theme: {
// Override table styling
table: "min-w-full border-2 border-blue-500 rounded-lg shadow-lg",
// Override row hover effect
row: "hover:bg-purple-100 transition-colors duration-200",
// Override button styling
button: "px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700",
// Override pagination active button
paginationButtonActive: "bg-purple-600 text-white font-bold",
// Override header styling
header: "bg-gradient-to-r from-blue-500 to-purple-500 text-white",
},
});
The DataTable includes a built-in loading spinner system that appears during data fetches and updates. This feature is customizable and integrates seamlessly with DaisyUI or any custom spinner element.
Enable and configure the spinner using the loading config object:
const table = new DataTable({
// ... other config options ...
loading: {
show: true, // Enable spinner
elementId: "custom-loading-spinner", // for custom spinners only (optional)
delay: 1000, // Delay (ms) before auto-hide
},
});
| Option | Type | Default | Description |
|---|---|---|---|
show |
boolean | false |
Whether to show the spinner during data operations |
elementId |
string | null |
ID of the custom DOM element to use as the spinner |
delay |
number | 0 |
How long (ms) to auto-hide the spinner after data loads (0 = manual hide) |
show: true is set, the spinner appears automatically during data fetch
operations.
elementId is provided, the DataTable will use your custom spinner element
instead of the default built-in spinner.delay is set to a value greater than 0, the spinner will automatically hide
after the specified delay (in milliseconds) once data loading completes.delay is 0, you must manually hide the spinner using the DataTable API
methods.
When elementId is not specified, the DataTable uses its built-in spinner. The default
spinner is automatically styled to match your selected theme (DaisyUI, Bootstrap, or Tailwind).
const table = new DataTable({
tableId: "datatable",
url: "/api/data",
columns: [...],
loading: {
show: true, // Uses default spinner
delay: 500, // Auto-hide after 500ms
},
});
You can use your own custom spinner element by providing the elementId option. Add your
spinner element in the HTML (initially hidden):
<div id="custom-loading-spinner" class="hidden">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
Then initialize the table with the spinner config:
const table = new DataTable({
tableId: "datatable",
url: "/api/data",
columns: [...],
loading: {
show: true,
elementId: "custom-loading-spinner",
delay: 1500,
},
});
The loading spinner automatically appears during the following operations:
reload() method)delay value (500-2000ms) to prevent spinner flickering on fast
network connectionsvanillajs-datatable provides powerful custom rendering capabilities that allow you to customize how
cell values are displayed. Using the render function, you can format data, add badges,
icons, buttons, and create complex
HTML structures for any column.
Add a render function to any column definition to customize how its values are
displayed:
const columns = [
{
name: 'id',
label: 'ID',
render: (value, row) => `#${value}`,
},
{
name: 'email',
label: 'Email',
},
];
The render function receives two parameters:
| Parameter | Type | Description |
|---|---|---|
value |
any | The cell value for the current column |
row |
object | The complete row data object, allowing access to all column values |
render function is called for each cell in the column during table rendering.
render function is provided, the raw value is displayed as a string.row parameter gives you access to all data in the current row, enabling
conditional rendering based on other column values.Format numbers, dates, or text with custom formatting:
const columns = [
{
name: 'id',
label: 'ID',
render: (value) => `#${value}`,
},
{
name: 'price',
label: 'Price',
render: (value) => `$${parseFloat(value).toFixed(2)}`,
},
{
name: 'created_at',
label: 'Created At',
render: (value) => new Date(value).toLocaleDateString(),
},
];
Use badges to display status, categories, or labels:
const columns = [
{
name: 'status',
label: 'Status',
render: (value) => {
const colors = {
'active': 'badge-success',
'inactive': 'badge-error',
'pending': 'badge-warning',
};
return `${value}`; }, }, { name: 'role', label: 'Role', render: (value) => `${value.toUpperCase()}`, }, ];
Use the row parameter to conditionally render based on other column values:
const columns = [
{
name: 'name',
label: 'Name',
render: (value, row) => {
const isActive = row.status === 'active';
return `${value}`;
},
},
{
name: 'priority',
label: 'Priority',
render: (value, row) => {
if (row.status === 'urgent') {
return `${value.toUpperCase()}`;
}
return `${value}`;
},
},
];
Here's a complete example combining multiple rendering techniques:
const table = new DataTable({
tableId: "datatable",
url: "/api/users",
columns: [
{
name: 'id',
label: 'ID',
render: (value) => `#${value}`,
},
{
name: 'name',
label: 'Name',
render: (value) => `${value}`,
},
{
name: 'email',
label: 'Email',
render: (value) => `${value}`,
},
{
name: 'status',
label: 'Status',
render: (value) => {
const badgeClass = value === 'active' ? 'badge-success' : 'badge-error';
return `${value}`;
},
},
{
name: 'created_at',
label: 'Created At',
render: (value) => new Date(value).toLocaleDateString(),
},
{
name: 'actions',
label: 'Actions',
render: (value, row) => {
return `
`;
},
},
],
});
row parameter when you need data from other columns for conditional
rendering
baseTheme setting
Custom rendering works seamlessly with other DataTable features:
highlightable: true
vanillajs-datatable provides a comprehensive set of column properties that allow you to customize column behavior, appearance, and functionality. Each column in your table configuration can use these properties to control visibility, export behavior, editing capabilities, styling, and more.
Every column definition must include these essential properties:
| Property | Type | Description |
|---|---|---|
name |
string | Required. The unique identifier for the column, must match the property name in your data objects |
label |
string | Required. The display text shown in the column header |
Here's a comprehensive list of all available column properties:
| Property | Type | Default | Description |
|---|---|---|---|
name |
string | — | Required. Column identifier that matches data property name |
label |
string | — | Required. Header label text |
render |
function | null |
Custom render function: (value,
row) => string |
highlightable |
boolean | object | false |
Enable search term highlighting. Use
true for the default yellow highlight or object
{highlightClass: string, class: string, cellClass: string} for
customization (the object keys mirror the helper props).
|
highlightableClass |
string | null |
CSS class added to highlight wrappers (
<mark>) when matches are found.
|
highlightableCellClass |
string | null |
Optional class applied to the cell
<td> whenever highlighted matches exist in the column.
|
exportable |
boolean | true |
Include column in Excel, CSV, and PDF
exports. Set to false to exclude |
printable |
boolean | true |
Include column in print view. Set to
false to exclude
|
visible |
boolean | true |
Initial column visibility state. Used with column visibility feature |
required |
boolean | false |
Prevent column from being hidden via column visibility controls |
editable |
boolean | false |
Enable inline editing (double-click to edit) |
type |
string | "text" |
Data type for editable cells:
"text",
"number", "date", "datetime-local",
"time", "select"
|
options |
array | [] |
Options array for
type: "select" columns
|
onCellEdit |
function | null |
Handler for cell edit events: async
({id, column, newValue}) => {...} |
tooltip |
string | null |
Tooltip text shown on column header hover |
align |
string | null |
Text alignment: "left",
"right", "center"
|
width |
string | null |
Column width (e.g., "200px",
"20%")
|
class |
string | null |
Additional CSS classes applied to cells |
group |
string | null |
Column group key for grouping columns under a header |
const columns = [
{
name: 'id',
label: 'ID',
required: true, // Cannot be hidden
},
{
name: 'email',
label: 'Email',
},
];
The highlightable property enables search term highlighting. When a user searches,
matching text in highlightable columns is automatically highlighted.
const columns = [
{
name: 'name',
label: 'Name',
highlightable: true,
highlightableClass: 'bg-yellow-100 text-yellow-900', // Class applied to highlight wrapper
highlightableCellClass: 'border border-yellow-200/50',
},
{
name: 'description',
label: 'Description',
highlightable: {
highlightClass: 'bg-red-200 text-red-900', // Alias for highlightableClass when using object form
tag: 'mark',
cellClass: 'border border-red-200/70', // Optional column-level cell class
},
},
];
How It Works:
highlightable: true, search terms are highlighted with the default theme color
(typically yellow)highlightClass/class and
cellClass for fine-grained control over the wrapper and the cell styling
highlightableClass and highlightableCellClass
let you configure the wrapper and cell classes outside the object for convenience
highlightable enabledrender function is usedControl which columns appear in exports and print views:
const columns = [
{
name: 'id',
label: 'ID',
exportable: true, // Include in Excel, CSV, PDF
printable: true, // Include in print view
},
{
name: 'actions',
label: 'Actions',
exportable: false, // Exclude from Excel, CSV, PDF
printable: false, // Exclude from print view
},
{
name: 'internal_notes',
label: 'Notes',
exportable: false, // Don't export
printable: true, // But allow printing
},
];
Control initial visibility and prevent columns from being hidden:
const columns = [
{
name: 'id',
label: 'ID',
required: true, // Cannot be hidden by user
visible: true,
},
{
name: 'email',
label: 'Email',
visible: true, // Initially visible, can be hidden
},
{
name: 'internal_id',
label: 'Internal ID',
visible: false, // Initially hidden
},
];
Enable inline editing with different input types:
const columns = [
{
name: 'name',
label: 'Name',
editable: true,
type: 'text',
async onCellEdit({id, column, newValue}) {
const response = await fetch('/api/users/update', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id, field: column.name, value: newValue}),
});
if (!response.ok) throw new Error('Update failed');
},
},
{
name: 'status',
label: 'Status',
editable: true,
type: 'select',
options: ['active', 'inactive', 'pending'],
},
{
name: 'price',
label: 'Price',
editable: true,
type: 'number',
},
{
name: 'created_at',
label: 'Created At',
editable: true,
type: 'date',
},
];
See the Editable Cells section for detailed documentation on these methods.
Customize column appearance and alignment:
const columns = [
{
name: 'price',
label: 'Price',
width: '120px', // Fixed width
class: 'font-bold text-green-600 text-end', // Additional CSS classes
},
{
name: 'status',
label: 'Status',
tooltip: 'Current account status', // Header tooltip
class: 'text-center',
},
];
Organize columns into groups:
const columns = [
{
name: 'first_name',
label: 'First Name',
group: 'personal', // Group key
},
{
name: 'last_name',
label: 'Last Name',
group: 'personal', // Same group
},
{
name: 'email',
label: 'Email',
group: 'contact', // Different group
},
];
Here's a comprehensive example using multiple column properties:
const columns = [
{
name: 'id',
label: 'ID',
required: true,
width: '80px',
class: 'text-end',
render: (value) => `#${value}`,
},
{
name: 'name',
label: 'Full Name',
tooltip: 'User full name',
highlightable: true,
editable: true,
type: 'text',
visible: true,
render: (value) => `${value}`,
async onCellEdit({id, column, newValue}) {
await updateUser(id, {[column.name]: newValue});
},
},
{
name: 'email',
label: 'Email Address',
highlightable: {
color: 'bg-blue-200',
tag: 'span',
},
exportable: true,
printable: true,
},
{
name: 'status',
label: 'Status',
editable: true,
type: 'select',
options: ['active', 'inactive', 'pending'],
class: 'text-center uppercase',
},
{
name: 'actions',
label: 'Actions',
exportable: false,
printable: false,
render: (value, row) => `
`,
},
];
Understanding how properties work together:
visible: false) are automatically
excluded from exports, regardless of exportable settingrequired: true) cannot be hidden
via column visibility controlstype property determines the input type when editing
cells
exportRender or
useRenderForExport to customize export output
name and label for every columnrequired: true for critical columns that should always be visibleexportable: false and printable: false for action columns or
internal datahighlightable: true on searchable text columns for better UXtooltip to provide additional context for column headerstype values for editable columns to get correct input controls
width sparingly - let the table auto-size when possiblerender with highlightable for rich, searchable contentInline editing is opt-in. Every editable column must set editable: true, declare a
type (defaults to "text"), and provide an onCellEdit
callback. When the type is "select", you must also supply an
options array so the dropdown has values.
| Property | Type | Description |
|---|---|---|
editable |
boolean | Flag to enable inline editing (double-click). |
type |
string | Input type ("text",
"number", "select", etc.). Required for editable columns.
|
options |
array | Required if type: "select";
defines dropdown values. |
onCellEdit |
function | Receives
{id, column, newValue}; persist the change here.
|
const columns = [
{
name: 'name',
label: 'Name',
editable: true,
type: 'text',
onCellEdit: async ({ id, column, newValue }) => {
await updateUser(id, { [column.name]: newValue });
},
},
{
name: 'status',
label: 'Status',
editable: true,
type: 'select',
options: ['active', 'inactive', 'pending'],
onCellEdit: async ({ id, column, newValue }) => {
await updateUser(id, { [column.name]: newValue });
},
},
];
type for editable columns so the right control renders.options for selects to avoid empty dropdowns.onCellEdit lean—validate or persist the value and throw if it fails.Column groups allow you to visually organize related columns under a shared heading. This improves readability when working with tables that contain many fields, making it easier for users to understand the data structure.
Use the columnGroups array to define groups by key and label:
const columnGroups = [
{
key: "personal",
label: "Personal Info",
headerClass: "bg-blue-100 text-center", // Optional: CSS classes for group header
},
{
key: "account",
label: "Account Details",
},
];
Each key should match the group property in your column definitions.
First, define your column groups:
const columnGroups = [
{ key: "personal", label: "Personal Info" },
{ key: "account", label: "Account Details" },
];
Then, assign columns to groups using the group property:
const columns = [
{
name: "name",
label: "Name",
group: "personal",
},
{
name: "email",
label: "Email",
group: "personal",
},
{
name: "username",
label: "Username",
group: "account",
},
{
name: "created_at",
label: "Created At",
group: "account",
},
];
Finally, pass the columnGroups array to the DataTable constructor:
const table = new DataTable({
tableId: "datatable",
url: "/users/datatable",
dataSrc: "users",
columns: columns,
columnGroups: columnGroups,
});
This will render a table header with grouped columns:
| Personal Info | Account Details | ||
|---|---|---|---|
| Name | Username | Created At | |
| John Doe | john@example.com | johndoe123 | 2024-05-01 |
| Jane Smith | jane@example.com | janesmith | 2024-06-15 |
group key will be included under that headinggroup property will appear without any group headingcolspanheaderClass propertyEach group object can have the following properties:
| Property | Type | Required | Description |
|---|---|---|---|
key |
string | Yes | Unique identifier for the group (must
match column group property) |
label |
string | Yes | Display text for the group header |
headerClass |
string | No | CSS classes to apply to the group header (e.g., "bg-blue-100 text-center") |
You can mix grouped and ungrouped columns in the same table:
const columnGroups = [
{ key: "personal", label: "Personal Info" },
];
const columns = [
{ name: "id", label: "ID" }, // No group - appears without group header
{ name: "name", label: "Name", group: "personal" },
{ name: "email", label: "Email", group: "personal" },
{ name: "status", label: "Status" }, // No group - appears without group header
];
You can customize the appearance of group headers using the headerClass property:
const columnGroups = [
{
key: "personal",
label: "Personal Info",
headerClass: "bg-blue-100 text-center font-bold", // Tailwind CSS classes
},
{
key: "account",
label: "Account Details",
headerClass: "bg-green-100 text-center font-bold",
},
];
Or use custom CSS:
const columnGroups = [
{
key: "personal",
label: "Personal Info",
headerClass: "custom-group-header", // Custom CSS class
},
];
If you run into problems, this section lists the most common issues and how to resolve them quickly.
hideExportProgress() is called in your onExportComplete()
callback.
onExportComplete that might prevent
hiding.
exportable config enables buttons so the modal has a cancel button.
table.clearFilters() and optionally table.goToPage(1) when you
reset.
table.redraw() after making batch
changes.
selection.enabled and selection.mode are configured before the
table renders.table.onSelectionChange() to show/hide UI based on selection count.scroll-margin-top to prevent hidden anchors.<style> block.
type, text or image,
and valid position.context.exportProgress.cancelController.signal.AbortController; polyfill if targeting older browsers.These recommendations keep the table responsive at scale:
searchDelay setting to prevent flood requests;
300-800ms is ideal.maxScrollPages and use
scrollWrapperHeight to control DOM size.
const table = new DataTable({
// ...
searchable: true,
searchDelay: 500, // only fetch after user stops typing for 500ms
});
Recommended practices for using vanillajs-datatable effectively.
columns explicitly: Include name and label
to avoid surprises.perPageOptions: Offer sensible row counts so users control density.columnVisibility with
saveState for persistence.
onExportError,
onSelectionChange) to show user feedback.
Version history and changes.
How to contribute to vanillajs-datatable.
License information for vanillajs-datatable.