Introduction

Overview

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.

Key Features

  • Pagination: Server-side and client-side pagination with customizable page sizes
  • Sorting: Multi-column sorting with ascending/descending order
  • Search: Global and column-specific search functionality
  • Filtering: Advanced filtering with multiple filter types
  • Column Visibility: Show/hide columns dynamically with state persistence
  • Row Selection: Single and multiple row selection with selection callbacks
  • Export: Export to PDF, Excel, CSV, JSON, and Print formats
  • Infinite Scroll: Load data as user scrolls for better UX
  • State Management: Save and restore table state (sorting, pagination, filters)
  • Event Handlers: Row click, cell click, and hover event handlers
  • Keyboard Navigation: Full keyboard support for accessibility
  • Theming: Support for Bootstrap, Tailwind CSS, and DaisyUI
  • Custom Rendering: Custom cell renderers and column formatting
  • Editable Cells: Inline cell editing with validation

Technology

vanillajs-datatable is built with:

  • JavaScript (ES6+): Modern JavaScript with async/await, classes, and modules
  • No Dependencies: Core library has no external dependencies
  • Optional Libraries: jsPDF for PDF export, ExcelJS for Excel export
  • CSS Frameworks: Compatible with Bootstrap, Tailwind CSS, and DaisyUI
  • Node >= 16: Node version 16 or higher is required

Installation

vanillajs-datatable can be installed and used in several ways:

NPM Installation (Recommended for Bundlers)

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
});

UMD / CDN Usage (Plain HTML)

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>

Local Files

You can also download the files and include them directly in your project.

Getting Started

Basic Setup

To get started with vanillajs-datatable, you need to:

  1. Install the package (via NPM or CDN)
  2. Create an HTML table element
  3. Configure and initialize the DataTable
  4. Set up your backend API to return data in the correct format

Configuration

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.

Backend API Format

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)

Core Features

Pagination

vanillajs-datatable supports powerful pagination controls to navigate large datasets efficiently.

Enable Pagination Controls

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.

Pagination Types

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...
});

How It Works

  • When pagination is enabled, only the current page's rows are shown.
  • Users can switch pages using the buttons or page numbers.
  • Pagination state updates automatically when filtering or sorting.

Server-Side Pagination

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.

Sorting

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.

Enabling Sorting

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.

Default Sort

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" },
    ],
});

How It Works

  • Click on a column header to sort by that column.
  • Click again to toggle between ascending and descending order.
  • Sort indicators (arrows) show the current sort column and direction.
  • Sorting state is preserved when paginating or filtering.

Server-Side Sorting

When sorting is enabled, vanillajs-datatable automatically sends sorting parameters to your backend API with each request.

Sorting Parameters Sent

  • 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

Backend Expectations

Your API should:

  • Return the data sorted by the specified column and order
  • Include total row count for pagination
  • Handle the sortBy and order parameters correctly

You 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 Search

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.

Debounce Delay

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.

How It Works

  • Users type in the search box above the table.
  • The search is debounced, meaning it waits for the user to stop typing before sending the request.
  • The search keyword is sent via a query parameter: ?search=keyword
  • Search works across all columns by default (global search).
  • Search resets pagination to page 1 automatically.
  • Search state is preserved when sorting or changing page size.

Server-Side Search

When search is enabled, vanillajs-datatable automatically sends the search parameter to your backend API with each request.

Search Parameter Sent

  • search — The search keyword entered by the user

Example Request:

GET /users/datatable?search=john&page=1&perPage=10&sortBy=name&order=asc

Backend Expectations

Your API should:

  • Search across relevant columns for the keyword
  • Return filtered results based on the search term
  • Include total count of filtered results for pagination
  • Handle empty search strings (return all data)

Custom Search Input

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.

Search Tips

  • Use appropriate debounce delay based on your server performance (300-800ms recommended).
  • Implement server-side search for large datasets to improve performance.
  • Consider indexing database columns that are frequently searched.
  • Clear search automatically resets to show all data.

Filtering

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 Search Filtering

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.

Basic Configuration

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" },
    ],
});

Feature Toggle

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
});

Key Features

Selective Column Filtering

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.

Real-time Filtering

  • Filters update as you type (with configurable debounce delay via searchDelay)
  • Pagination automatically resets to page 1 when filtering
  • Works alongside global search for combined filtering
  • Multiple column filters can be active simultaneously

How It Works

  • Filter inputs appear above column headers for specified columns
  • Users type in the filter input to filter that specific column
  • Filters are debounced to reduce server requests during typing
  • Filter values are sent as query parameters: ?columnFilters[name]=value&columnFilters[email]=value
  • All active filters are combined with AND logic
  • Filters work together with global search (AND logic)

Server-Side Filtering

When column filtering is enabled, vanillajs-datatable automatically sends filter parameters to your backend API with each request.

Filter Parameters Sent

Column filters are sent as an object in the query string:

  • columnFilters[name] — Filter value for the "name" column
  • columnFilters[email] — Filter value for the "email" column
  • Each filterable column gets its own parameter

Example Request:

GET /users/datatable?columnFilters[name]=john&columnFilters[email]=example&page=1&perPage=10

Backend Expectations

Your API should:

  • Filter each column based on its corresponding filter value
  • Apply all column filters with AND logic (all must match)
  • Combine column filters with global search (if present)
  • Return filtered results with correct total count
  • Handle empty filter values (ignore that filter)

You can also control filters programmatically using the Filter API:

See the Filter API section for detailed documentation on these methods.

Filtering Tips

  • Use column filtering for precise, column-specific searches
  • Combine with global search for powerful filtering capabilities
  • Configure appropriate debounce delay to balance responsiveness and server load
  • Only enable filtering for columns that benefit from it
  • Consider database indexing for frequently filtered columns

Column Visibility

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.

Configuration

Basic Setup

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
});

Configuration Options

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)

Usage

UI Button

When enabled, a "Columns" button appears in the DataTable controls. Click it to open a dropdown menu with:

  • Checkboxes for each column (toggle visibility)
  • "Show All" button
  • "Hide All" button
  • "Reset" button (restore initial state)
  • Required columns are marked and cannot be hidden

How It Works

  • Users click the "Columns" button to open the visibility menu
  • Checkboxes show the current visibility state of each column
  • Clicking a checkbox toggles that column's visibility
  • Changes are applied immediately to the table
  • If persistState: true and saveState: true, the state is saved to localStorage
  • Required columns (like ID or actions) cannot be hidden

Column Configuration

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.

Use Cases

  • Customizable Views: Allow users to customize their table view based on their needs
  • Mobile Optimization: Hide less important columns on smaller screens
  • Role-Based Views: Different users can see different columns
  • Print Optimization: Hide columns that aren't needed for printing

Best Practices

  • Enable state persistence for better user experience
  • Mark important columns (like ID, actions) as required (visible: false)
  • Provide a reset option so users can restore default view
  • Consider mobile users when allowing column hiding
  • Test that required functionality (like action buttons) remains accessible

Selectable Rows

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.

Basic Configuration

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
    },
});

Configuration Options

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

How It Works

  • When enabled: true, users can click on table rows to select them.
  • In mode: "single", clicking a row deselects any previously selected row and selects the clicked row.
  • In mode: "multiple", users can select multiple rows by clicking them. Clicking a selected row deselects it.
  • Selected rows automatically receive the CSS class(es) specified in selectedClass.
  • Selection state is maintained when paginating, filtering, or sorting.
  • You can programmatically control selection using the Selection API methods.

Single Selection Mode

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:

  • Clicking a row automatically deselects any previously selected row
  • Only one row can be selected at any time
  • Useful for detail views, editing, or single-item operations

Multiple 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:

  • Users can click multiple rows to select them
  • Clicking a selected row deselects it
  • Useful for batch operations, bulk exports, or multi-item actions
  • Works well with checkboxes or action buttons for selected items

Customizing Selection Styling

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.

Best Practices

  • Use mode: "single" for detail views, editing, or single-item operations
  • Use mode: "multiple" for batch operations, bulk exports, or multi-item actions
  • Choose selectedClass styles that provide clear visual feedback without being distracting
  • Use onSelectionChange callbacks to update UI elements (buttons, counters) based on selection state
  • Clear selection programmatically after performing batch operations for better UX
  • Consider accessibility - ensure selected rows have sufficient contrast and visual indicators
  • Test selection behavior with pagination, search, and filtering to ensure state persistence works correctly
  • For large datasets, consider performance implications of maintaining selection state across operations

Infinite Scroll

Enable 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.

Basic Usage

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
    },
});

Configuration Options

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.)

How It Works

  • When enabled, the table creates a scrollable container with the specified height
  • As the user scrolls down, the system monitors scroll position
  • When the user reaches within scrollOffset pixels of the bottom, the next page is automatically loaded
  • New rows are appended to the existing table data
  • The process continues until maxScrollPages is reached
  • If hidePaginationOnScroll is enabled, pagination controls are hidden automatically

Scroll Offset Configuration

The scrollOffset determines how close to the bottom the user must scroll before triggering the next page load:

  • Smaller values (5-10px): More sensitive, triggers loading sooner
  • Larger values (50-100px): Less sensitive, triggers loading later
  • Recommended: 10-20px for smooth user experience
infiniteScroll: {
    enabled: true,
    scrollOffset: 20, // Trigger 20px before reaching bottom
}

Scroll Container Height

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.

Server-Side Integration

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
}

Use Cases

  • Large Datasets: Load data progressively without overwhelming the browser
  • Mobile UX: Provide smooth scrolling experience on mobile devices
  • Streamlined Interface: Hide pagination for cleaner UI
  • Social Media Feeds: Similar to infinite scroll in social media applications
  • Product Listings: Load products as user browses

Best Practices

  • Use appropriate scrollOffset values (10-20px recommended)
  • Set maxScrollPages to prevent excessive data loading
  • Enable hidePaginationOnScroll for cleaner UI when using infinite scroll
  • Use viewport-based heights (vh) for responsive design
  • Test with large datasets to ensure smooth performance
  • Consider server performance when setting chunk sizes
  • Provide visual feedback (loading indicators) during data fetching

Limitations and Notes

  • Works best with server-side data loading or paginated APIs
  • Will stop loading once maxScrollPages is reached
  • You can use a smaller scrollOffset for faster triggering, or increase it to reduce sensitivity
  • If hidePaginationOnScroll is enabled, the pagination UI is hidden automatically while scrolling
  • Infinite scroll is mutually exclusive with traditional pagination when hidePaginationOnScroll: true
  • Scroll position is maintained when new data is loaded
  • Performance may degrade with very large datasets (consider virtual scrolling for extreme cases)

Example: Complete Configuration

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
});

State Management

Save and restore table state including sorting, pagination, filters, and column visibility. State is persisted in localStorage.

Items Per Page

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.

Basic Configuration

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
});

Configuration Options

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)

How It Works

  • The perPage value determines how many rows are initially displayed and sent to the server in API requests.
  • When perPageSelector: true, a dropdown appears in the table controls allowing users to change the page size.
  • The perPageOptions array defines the available choices in the dropdown.
  • When a user selects a different page size, the table automatically reloads data with the new perPage value.
  • The selected page size is sent to your backend API as a query parameter: ?perPage=100
  • If perPageSelector: false, users cannot change the page size, and the table uses the perPage value you specified.

Setting Default Page Size

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.

Customizing Per Page Options

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,
});

Hiding the Per Page Selector

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
});

Server-Side Integration

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:

  • Accept the perPage query parameter
  • Return the appropriate number of records based on the perPage value
  • Include pagination metadata in the response:
{
    "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)
}

Programmatic Control

You can change the page size programmatically using the Pagination API:

See the Pagination API section for more details on programmatic pagination control.

Best Practices

  • Choose a perPage value that balances performance and usability (10-100 is typical)
  • Include the default perPage value in your perPageOptions array for consistency
  • Order perPageOptions in ascending order for better user experience
  • Consider your server's performance when setting large perPage values (e.g., 500+)
  • Use reasonable increments in perPageOptions (e.g., [10, 25, 50, 100] rather than [10, 11, 12, 13])
  • If using state persistence, ensure your backend can handle the saved perPage value
  • Test with different perPage values to ensure your API handles them correctly
  • For very large datasets, consider limiting maximum perPage to prevent performance issues

API Methods

vanillajs-datatable provides a comprehensive API for interacting with the table programmatically. All API methods are organized by category.

Event Handlers

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.

Overview

The Event Handler API consists of six main methods:

  • Registration Methods: onRowClick(), onCellClick(), onRowHover()
  • Removal Methods: removeRowClickHandlers(), removeCellClickHandlers(), removeRowHoverHandlers()

Features

  • Multiple Callbacks: Register multiple handlers for the same event type - all will be executed
  • Event Delegation: Automatically handles dynamically added rows without re-registering handlers
  • Smart Filtering: Automatically ignores clicks on buttons, links, inputs, and other interactive elements
  • Rich Data: Provides complete row data, element references, and native event objects
  • Easy Cleanup: Remove all handlers when no longer needed

onRowClick(callback)

Register a callback function that will be called when a user clicks on a table row.

Parameters

Parameter Type Description
callback Function Callback function with signature: (rowId, rowData, rowElement, event)

Callback Parameters

  • rowId (string|number) - The ID of the clicked row
  • rowData (Object) - Complete row data object containing all column values
  • rowElement (HTMLElement) - The DOM element of the clicked row
  • event (Event) - Native click event object (allows checking modifier keys)

Basic Usage

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}`;
});

Multiple Callbacks

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);
});

Important Notes

  • Clicks on buttons, links, inputs, selects, textareas, and elements with role="button" are automatically ignored
  • The callback receives the complete row data object, not just the ID
  • The native event object allows you to check for modifier keys (Ctrl, Shift, Alt, Meta)
  • Event delegation ensures handlers work with dynamically added rows

onCellClick(callback)

Register a callback function that will be called when a user clicks on a table cell.

Parameters

Parameter Type Description
callback Function Callback function with signature: (rowId, columnName, cellValue, cellElement, rowElement, rowData, event)

Callback Parameters

  • rowId (string|number) - The ID of the row containing the clicked cell
  • columnName (string) - The name of the clicked column
  • cellValue (string) - The text content of the clicked cell
  • cellElement (HTMLElement) - The DOM element of the clicked cell
  • rowElement (HTMLElement) - The DOM element of the row
  • rowData (Object) - Complete row data object
  • event (Event) - Native click event object

Basic Usage

table.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}`;
    }
});

Conditional Handling by Column

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");
    }
});

Visual Feedback Example

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);
});

onRowHover(callback)

Register a callback function that will be called when a user hovers over a table row (mouseenter event).

Parameters

Parameter Type Description
callback Function Callback function with signature: (rowId, rowData, rowElement, event)

Callback Parameters

  • rowId (string|number) - The ID of the hovered row
  • rowData (Object) - Complete row data object
  • rowElement (HTMLElement) - The DOM element of the hovered row
  • event (Event) - Native mouseenter event object

Basic Usage

table.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";
});

Tooltip Example

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";
    }
});

Removing Handlers

Remove all registered callbacks for specific event types. This is useful for cleanup or dynamic handler management.

Methods

  • removeRowClickHandlers() - Remove all row click callbacks
  • removeCellClickHandlers() - Remove all cell click callbacks
  • removeRowHoverHandlers() - Remove all row hover callbacks

Usage

// Remove all row click handlers
table.removeRowClickHandlers();

// Remove all cell click handlers
table.removeCellClickHandlers();

// Remove all row hover handlers
table.removeRowHoverHandlers();

Cleanup Example

// 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();
}

Complete Example

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();
});

Best Practices

  • Use row click handlers for navigation, detail views, or row-level actions
  • Use cell click handlers for column-specific actions (email links, phone calls, inline editing)
  • Use row hover handlers for previews, tooltips, or visual feedback
  • Register multiple callbacks when you need to separate concerns (navigation, analytics, UI updates)
  • Always clean up handlers when components unmount or handlers are no longer needed
  • Check for modifier keys (Ctrl, Shift, Alt) in callbacks for enhanced user experience
  • Use the native event object to prevent default behavior or stop propagation when needed
  • Consider performance when registering handlers on large tables - event delegation is handled automatically
  • Test handlers with dynamically added rows to ensure they work correctly

Integration with Other Features

Event handlers work seamlessly with other DataTable features:

  • Row Selection: Event handlers can work alongside row selection - handlers fire before selection changes
  • Pagination: Handlers automatically work with paginated data - no need to re-register
  • Search/Filtering: Handlers work with filtered results - row IDs remain consistent
  • Sorting: Handlers continue to work after sorting - row data is always current
  • Editable Cells: Cell click handlers can trigger inline editing or custom edit dialogs

Column Visibility API

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.

Methods

  • getVisibleColumns() – Returns all currently visible column objects
  • isColumnVisible(columnName) – Checks whether the named column is visible
  • toggleColumnVisibility(columnName, visible?) – Toggle or force visibility
  • showColumn(columnName) / hideColumn(columnName) – Convenience wrappers
  • showAllColumns() / hideAllColumns() – Bulk show/hide (required columns stay visible)
  • resetColumnVisibility() – Restore each column's initial visibility state
  • bindColumnVisibilityButton() – Internal helper for wiring the controls dropdown

Basic Example

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));

Method Details

getVisibleColumns()
Returns the current visible columns (filters out visible: false and hidden state from user toggles).
isColumnVisible(columnName)
Respects the runtime visibility state and optional column.visible metadata.
toggleColumnVisibility(columnName, visible?)
Toggles visibility for the column. Pass true/false to force a state. Required columns cannot be hidden.
showAllColumns() / hideAllColumns()
Bulk actions that respect required: true columns.
resetColumnVisibility()
Restore each column to its initial visible setting (true unless you explicitly set visible: false).
bindColumnVisibilityButton()
Internal helper that wires the dropdown and buttons. Happens automatically when you enable columnVisibility.showButton.

Action Button Examples

// 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");
});

Integration with the Dropdown

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 buttons
  • Required columns annotated with “(required)”
  • Persistent state when persistState: true and saveState: true

Best Practices

  • Mark essential columns with required: true so users can’t hide them.
  • Use toggleColumnVisibility to build presets (e.g., compact vs. detailed views).
  • Hide columns before rendering export files if you also hide them from the UI.
  • Call showAllColumns() before exporting so hidden columns are included.
  • Ensure your backend handles visible columns if you’re serializing view state.

See the Column Visibility Guide for detailed control examples.

Pagination API

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.

Available Methods

  • goToPage(pageNumber) – Jump to a specific page
  • setPageSize(size) – Update the number of rows shown per page
  • getCurrentPage() – Read the current page index
  • nextPage() / prevPage() – Move forward or backward
  • firstPage() / lastPage() – Jump to the very start or end

Method Details

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

Usage Example

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);

Best Practices

  • Validate user input before calling goToPage() to avoid warnings
  • Reset to page 1 after changing the page size via setPageSize()
  • Use firstPage() and lastPage() when building keyboard shortcuts or jump links
  • Combine with saveState: true to persist the user-selected page size and page number
  • When using infinite scroll, only use pagination methods for the fallback pagination UI

Integration Notes

  • Per Page Selector: The pagination API updates the row count so the selector stays in sync
  • Search & Filtering: Call goToPage(1) after running a search to avoid empty pages
  • Sorting: Sorting retains the current page; you can call goToPage(1) if you want to reset view
  • State Persistence: Saved state restores the current page and rows per page automatically when enabled

Selection API

The 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.

Methods Overview

  • getSelectedIds() – Return IDs of currently selected rows
  • getSelectedRows() / getSelectedData() – Access DOM elements or data
  • getSelectedCount() – Total selected rows (useful for badges)
  • clearSelection() / selectAll() – Reset or bulk select
  • toggleRowSelection(id, force) – Select/deselect a single row programmatically
  • isSelected(id) – Check whether a row is already selected
  • selectRange(from, to) – Select a contiguous range by ID
  • setSelection(ids) – Replace selection with a custom set
  • invertSelection() – Flip every row’s selection state
  • setSelectable(flag) / setSelectMode(mode) – Toggle selection APIs on/off
  • destroySelectable() – Remove selection behavior entirely

Method Details

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

Usage Example

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();

Best Practices

  • Always call clearSelection() after completing a batch action to reset UI state
  • Use selectRange() or setSelection() for keyboard-friendly multi-select shortcuts
  • Use onSelectionChange() to enable/disable bulk action buttons
  • Wrap selection logic in guard clauses when selection.enabled can be toggled at runtime
  • Call destroySelectable() before reinitializing the table to avoid duplicate handlers

Data API

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.

Methods Overview

  • getData() – Returns the current dataset (after filters/sorts)
  • getRowData(rowId) / getRowIndex(rowId) – Read a single row
  • getRowsBy(field, value) / findRowsByFieldContains(field, value) – Field-based lookups
  • addRow(rowData, silent?) / addRows(rowsData, silent?) – Append data
  • updateRow(rowId, updates, silent?) / updateRows(rows, silent?) – Modify rows
  • deleteRow(rowId) / deleteRows(ids) – Remove rows by ID
  • redraw() / draw() – Re-render table without refetching

Usage Examples

These 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();

Method Details

getData()
Returns the currently rendered dataset, respecting sorting/filtering.
getRowData(rowId)
Look up a specific row by its id.
getRowIndex(rowId)
Returns the numeric index (zero-based) of the row in the current dataset.
getRowsBy(field, value)
Returns rows where row[field] === value.
findRowsByFieldContains(field, value)
Performs a case-insensitive contains search inside a field.
addRow(rowData, silent?)
Adds a single row; pass true for silent to skip redrawing immediately.
addRows(rowsData, silent?)
Adds multiple rows; best used with silent and a final redraw().
updateRow(rowId, updates, silent?)
Merge updates into the row, optionally skipping redraw.
updateRows(rows, silent?)
Updates multiple rows at once.
deleteRow(rowId)
Removes a row by ID.
deleteRows(ids)
Removes multiple rows.
redraw() / draw()
Re-render the table using the current data (no network calls).

Event-Based Example

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();

Best Practices

  • Always provide a unique id field when adding rows.
  • Use silent updates plus a single redraw() to avoid flickering.
  • Call getSelectedData() before deleting rows to confirm what will be removed.
  • Combine findRowsByFieldContains with custom filters for search helpers.
  • When using server-side data, keep client updates in sync with the server.

Filter API

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.

Available Methods

  • setFilter(key, value, silent?) – Set a filter value, with optional silent mode
  • removeFilter(key) – Remove a specific filter
  • clearFilters() – Reset all filters (useful when resetting state)

Usage Example

// Apply a filter without immediately fetching (silent)
table.setFilter("status", "active", true);

// Remove a single filter
table.removeFilter("status");

// Clear all filters
table.clearFilters();

How It Works

  • setFilter writes to context.filters; if silent is false it refetches the data
  • removeFilter deletes a key so the value no longer travels to the backend
  • clearFilters wipes the entire filters map; typically followed by table.goToPage(1)

Triggering Filtered Searches

Combine filter calls with the Pagination API for reactive UIs:

table.setFilter("category", "sales");
table.goToPage(1); // reset to first page after filtering

Best Practices

  • Use silent: true when you plan to batch several filters before reloading
  • Call clearFilters() as part of your global reset button
  • Persist filter values via saveState: true if you want users to return to the same view

Integration Notes

  • Search: Filters play nicely with the global search input—both send query params to your backend
  • Column Visibility: Hidden columns can still participate in filtering
  • State Persistence: Saved filters are restored when saveState: true is enabled

Sorting API

Use the Sorting API to programmatically apply or clear sort orders. These helpers respect your configured sortable columns and trigger data reloads automatically.

Methods Overview

  • setSort(column, direction = "asc") – Sort by a column and direction
  • clearSort() – Remove the current sort order (resets to defaults)

Usage Example

// Sort by name descending
table.setSort("name", "desc");

// Clear sorting
table.clearSort();

How It Works

  • setSort updates context.sort and context.order, then fetches new data
  • clearSort restores the default sort column/order and refetches data

Best Practices

  • Call goToPage(1) after applying a new sort to avoid empty pages
  • Keep your UI in sync: update sort indicators when calling these methods
  • Combine with filters and search—sorting works on the currently filtered dataset

Utility API

Utility 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.

copyToClipboard(format = "csv")

Copies the currently rendered table rows (respecting sorting, filtering, and column visibility) to the\n+ system clipboard.

Parameters

Parameter Type Description
format string "csv" (default) or "tsv" to toggle delimiters

How It Works

  • Gathers the currently visible columns (honors column.visible and requested visibility state)
  • Builds headers from column labels or names
  • Serializes each row value, using comma for CSV or tab for TSV
  • Writes the joined string to navigator.clipboard and logs success/failure

Example

document.getElementById("copy-csv").addEventListener("click", () => {
    table.copyToClipboard("csv");
});

document.getElementById("copy-tsv").addEventListener("click", () => {
    table.copyToClipboard("tsv");
});

Best Practices

  • Call this helper after the table renders to ensure there are rows available
  • Inform users when copying succeeds (show a toast or disable buttons briefly)
  • Use copyToClipboard("tsv") when pasting into spreadsheets that expect tab delimiters

Export Features

vanillajs-datatable provides built-in export options that let users download the table data in various formats or print it directly from the UI.

Supported Export Formats

  • CSV – Download as .csv file
  • Excel – Download as .xlsx file
  • PDF – Generate a styled .pdf file
  • Print – Open a print preview window
  • JSON – Export data as JSON (all, selected, or current page)

Enabling Export Options

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"
        },
    },
});

Export Buttons Overview

When exportable.enabled is true, the following buttons can be individually toggled:

Button Key Description
Print print Opens print preview window
Excel excel Downloads .xlsx file
CSV csv Downloads .csv file
PDF pdf Downloads a styled PDF document

Server-Side Export with Chunking

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.

PDF Export

Export table data to PDF with customizable styling, headers, footers, and custom elements.

PDF Options

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)
    },
}

Excel Export

Export to Excel format (.xlsx) with formatting and styling options. Excel exports preserve column formatting and support large datasets through chunking.

CSV Export

Export to CSV format with proper escaping and encoding. CSV exports are lightweight and compatible with spreadsheet applications.

JSON Export

Export data as JSON. Supports exporting:

  • All records (fetches all data from server)
  • Selected rows (only currently selected rows)
  • Current page (only visible rows)

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.

Export Customization

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.).

Custom Element Options

  • type: "text" or "image"
  • text/content: The string to render (PDF only)
  • image: URL, File, or base64 for logos
  • position: e.g., "top-right", "center", "bottom-left" (or "custom" with explicit coordinates)
  • fontSize, fontStyle, color, opacity
  • repeatOnPages: When true, renders on every page

Example

exportable: {
    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,
            },
        ],
    },
}

How It Works

  • calculatePosition() converts human-friendly positions into x/y coordinates.
  • applyTextToPdf()/applyImageToPdf() draw content via jsPDF.
  • Images can be loaded via URL, File, or inline base64.

Best Practices

  • Keep watermark text low opacity and avoid obstructing table data.
  • Use repeatOnPages for headers/footers.
  • Ensure image sources are accessible before exporting.

Export Progress

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.

Progress Methods

  • 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).

Usage Example

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();
});

Integration Notes

  • The modal uses theme classes such as exportProgressOverlay and exportProgressBar.
  • The floating toggle button reopens the modal when closed.
  • Cancelling sets exportProgress.isActive to false and aborts requests.

Important Notes

  • Export buttons only appear if exportable.enabled = true
  • File names should not include file extensions manually - they are added automatically
  • For large data, chunking avoids browser overload or server timeout
  • Export progress is tracked automatically with visual indicators
  • Users can cancel exports in progress
  • Custom elements (logos, watermarks) are supported in PDF and Print exports
  • Column visibility settings are respected in exports
  • Columns marked with exportable: false are excluded from all exports

Themes & Styling

vanillajs-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.

Available Base Themes

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.

How to Use Themes

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.

Example: Initialize with DaisyUI Theme (Default)

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",
    },
});

Example: Initialize with Bootstrap Theme

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",
    },
});

Example: Initialize with Tailwind Theme

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",
    },
});

Customizable Theme Keys

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

Custom Theme Override Example

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",
    },
});

Theme Best Practices

  • Choose a base theme that matches your existing UI framework
  • Override only the keys you need to customize
  • Use consistent styling across all overrides
  • Test your custom theme with all table features (pagination, filters, etc.)
  • Keep accessibility in mind (contrast ratios, focus states)

Loading Spinner

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.

Basic Usage

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
    },
});

Configuration Options

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)

How It Works

  • When show: true is set, the spinner appears automatically during data fetch operations.
  • If elementId is provided, the DataTable will use your custom spinner element instead of the default built-in spinner.
  • The spinner is shown when data is being fetched and hidden when the operation completes.
  • If delay is set to a value greater than 0, the spinner will automatically hide after the specified delay (in milliseconds) once data loading completes.
  • When delay is 0, you must manually hide the spinner using the DataTable API methods.

Default Spinner

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
    },
});

Custom Spinner Element

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,
    },
});

Spinner Behavior

The loading spinner automatically appears during the following operations:

  • Initial data fetch when the table is first loaded
  • Data refresh operations (e.g., when using reload() method)
  • Pagination changes (when loading new pages)
  • Sorting operations (when fetching sorted data)
  • Filtering and search operations (when fetching filtered results)

Best Practices

  • Set an appropriate delay value (500-2000ms) to prevent spinner flickering on fast network connections
  • Use custom spinners that match your application's design system
  • Ensure your custom spinner element is properly positioned (centered or overlay) for optimal user experience
  • Test spinner behavior with slow network conditions to ensure proper visibility
  • Consider using a delay of at least 300ms to avoid showing the spinner for very fast operations

Custom Rendering

vanillajs-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.

Basic Usage

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',
    },
];

Render Function Parameters

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

How It Works

  • The render function is called for each cell in the column during table rendering.
  • You can return any valid HTML string from the render function.
  • The returned HTML is inserted directly into the cell, allowing full customization.
  • If no render function is provided, the raw value is displayed as a string.
  • The row parameter gives you access to all data in the current row, enabling conditional rendering based on other column values.

Common Use Cases

Formatting 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(),
    },
];

Adding Badges and Status Indicators

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()}`, }, ];
                
                

Conditional Rendering

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}`;
        },
    },
];

Complete Example

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 `
                    
                    
                `;
            },
        },
    ],
});

Best Practices

  • Keep render functions simple and focused on presentation logic
  • Use the row parameter when you need data from other columns for conditional rendering
  • Ensure returned HTML is valid and properly escaped to prevent XSS vulnerabilities
  • Consider performance when rendering complex HTML structures for large datasets
  • Use theme-appropriate classes (DaisyUI, Bootstrap, or Tailwind) that match your baseTheme setting
  • Test render functions with various data types (null, undefined, empty strings) to handle edge cases
  • For complex rendering logic, consider extracting the function to a separate utility function for reusability

Integration with Other Features

Custom rendering works seamlessly with other DataTable features:

  • Search: Rendered HTML is searchable if the column has highlightable: true
  • Export: The original cell value (not rendered HTML) is used in exports unless specifically handled
  • Sorting: Sorting uses the original cell value, not the rendered output
  • Filtering: Column filters work with the original cell values
  • Editable Cells: When a cell is edited, the original value is used, not the rendered HTML

Column Properties

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.

Required Properties

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

Complete Property Reference

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

Property Usage Examples

Basic Column Definition

const columns = [
    {
        name: 'id',
        label: 'ID',
        required: true, // Cannot be hidden
    },
    {
        name: 'email',
        label: 'Email',
    },
];

Search Highlighting

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:

  • When highlightable: true, search terms are highlighted with the default theme color (typically yellow)
  • The object form now also accepts highlightClass/class and cellClass for fine-grained control over the wrapper and the cell styling
  • The helper properties highlightableClass and highlightableCellClass let you configure the wrapper and cell classes outside the object for convenience
  • Highlighting only works on columns with highlightable enabled
  • The highlighting applies to the rendered output if a render function is used

Export Control

Control 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
    },
];

Column Visibility

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
    },
];

Inline Editing

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.

Styling and Layout

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',
    },
];

Column Groups

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
    },
];

Complete Example

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) => `
            
        `,
    },
];

Property Interactions

Understanding how properties work together:

  • Visibility & Export: Hidden columns (visible: false) are automatically excluded from exports, regardless of exportable setting
  • Required & Visibility: Required columns (required: true) cannot be hidden via column visibility controls
  • Render & Highlightable: Highlighting works on the rendered HTML output when both properties are used
  • Editable & Type: The type property determines the input type when editing cells
  • Export & Render: By default, exports use raw values. Use exportRender or useRenderForExport to customize export output

Best Practices

  • Always include name and label for every column
  • Use required: true for critical columns that should always be visible
  • Set exportable: false and printable: false for action columns or internal data
  • Enable highlightable: true on searchable text columns for better UX
  • Use tooltip to provide additional context for column headers
  • Set appropriate type values for editable columns to get correct input controls
  • Use width sparingly - let the table auto-size when possible
  • Combine render with highlightable for rich, searchable content

Editable Cells

Inline 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.

Example

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 });
        },
    },
];

Tips

  • Always define type for editable columns so the right control renders.
  • Populate options for selects to avoid empty dropdowns.
  • Keep onCellEdit lean—validate or persist the value and throw if it fails.
  • Limit editing to essential columns to reduce accidental changes.

Column Groups

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.

Defining Groups

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.

Grouped Column Example

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,
});

Visual Result

This will render a table header with grouped columns:

Personal Info Account Details
Name Email Username Created At
John Doe john@example.com johndoe123 2024-05-01
Jane Smith jane@example.com janesmith 2024-06-15

Grouping Notes

  • Groups appear above individual column headers as a separate row
  • Only columns with a matching group key will be included under that heading
  • Columns without a group property will appear without any group heading
  • Groups span across multiple columns using colspan
  • You can style group headers using the headerClass property

Column Group Configuration

Each 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")

Mixed Columns Example

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
];

Styling Group Headers

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
    },
];

Use Cases

  • Organized Data Display: Group related fields together for better readability
  • Large Tables: Make complex tables with many columns more manageable
  • Logical Grouping: Group columns by category (e.g., Personal, Contact, Financial)
  • User Experience: Help users quickly locate relevant information

Best Practices

  • Use meaningful group names that clearly describe the column category
  • Keep groups logically related (e.g., all personal information together)
  • Don't create too many groups - 2-4 groups is usually optimal
  • Use consistent styling across all group headers
  • Consider mobile responsiveness when grouping columns
  • Test that column visibility still works correctly with groups

Limitations

  • Column groups are visual only - they don't affect data filtering or sorting
  • Export functionality respects column groups in PDF/Print exports
  • Column visibility toggle works independently of groups
  • Groups must be defined before columns are assigned to them

Troubleshooting

If you run into problems, this section lists the most common issues and how to resolve them quickly.

Common Issues

Export Progress Modal Stuck

  • Make sure hideExportProgress() is called in your onExportComplete() callback.
  • Check the browser console for errors inside onExportComplete that might prevent hiding.
  • Ensure your exportable config enables buttons so the modal has a cancel button.

Filters Not Clearing

  • Call table.clearFilters() and optionally table.goToPage(1) when you reset.
  • If you use silent filters, remember to call table.redraw() after making batch changes.

Selection Buttons Hidden

  • Ensure selection.enabled and selection.mode are configured before the table renders.
  • Use table.onSelectionChange() to show/hide UI based on selection count.

Pagination Controls Jump When Linking Heads

  • Scroll to headings uses scroll-margin-top to prevent hidden anchors.
  • If you still hit hidden headers, increase the margin in the <style> block.

Export Custom Elements Not Showing

  • Check element definitions include type, text or image, and valid position.
  • Ensure image URLs are accessible and preloaded; try base64 for remote assets.

Export Cancellation Fails

  • Verify your fetch loop checks context.exportProgress.cancelController.signal.
  • Ensure browsers support AbortController; polyfill if targeting older browsers.

Performance Tips

These recommendations keep the table responsive at scale:

  • Server-side pagination: Always paginate large data sets instead of fetching everything at once.
  • Debounce search: Use the searchDelay setting to prevent flood requests; 300-800ms is ideal.
  • Lazy exports: Exports fetch all data in chunks; avoid triggering multiple exports simultaneously.
  • Column visibility: Hide heavy columns (HTML, images) when not needed to reduce render cost.
  • Infinite scroll caution: Limit maxScrollPages and use scrollWrapperHeight to control DOM size.
  • Virtualized lists: For extremely large DOMs, consider integrating virtualization with the table body.
  • Profiling: Monitor network and paint events while applying filters/sorts to identify slow paths.

Example: Debounced Search

const table = new DataTable({
    // ...
    searchable: true,
    searchDelay: 500, // only fetch after user stops typing for 500ms
});

Best Practices

Recommended practices for using vanillajs-datatable effectively.

  • Define columns explicitly: Include name and label to avoid surprises.
  • Use perPageOptions: Offer sensible row counts so users control density.
  • Manage hidden column state: Pair columnVisibility with saveState for persistence.
  • Customize themes: Override theme keys instead of editing the core template.
  • Validate exports: Test PDF/Excel output after adding custom elements or filters.
  • Gracefully handle errors: Use the callback hooks (onExportError, onSelectionChange) to show user feedback.

More Info

Changelog

Version history and changes.

Contributing

How to contribute to vanillajs-datatable.

License

License information for vanillajs-datatable.