AdminKitDocs
Plugins

Next.js DataTables

Next.js DataTables for the App Router: a client-only DataTable loaded with next/dynamic, rendered from server component pages, with Bootstrap 5 styling.

Pro

This component is only available in AdminKit PRO. Learn more.

A long list of orders or employees is easier to work with when people can search it, sort it and page through it. DataTable adds DataTables to the pages of AdminKit for Next.js. It is built on the official React component of DataTables, datatables.net-react, with the Bootstrap 5 styling. It also loads four extensions: Buttons (with its HTML5 export and print buttons), Responsive, FixedHeader and Select.

DataTables builds the table by changing the page in the browser. It cannot run on the server, or during the static export that turns your app into HTML files. So src/components/plugins/datatable.tsx loads it with next/dynamic and ssr: false, which means "only load this in the browser":

src/components/plugins/datatable.tsx
"use client";

import dynamic from "next/dynamic";
import { Skeleton } from "@/components/ui/skeleton";

export const DataTable = dynamic(() => import("./datatable-client"), {
  ssr: false,
  loading: () => (
    <div className="position-relative" style={{ minHeight: 300 }}>
      <Skeleton />
    </div>
  ),
});

Next.js only allows ssr: false in a client component, a file that starts with "use client". That is why the file has it. The HTML that Next.js builds for the page has a 300px skeleton, not the rows. In the browser, Next.js downloads DataTables as a separate JavaScript file (a chunk), and datatable-client.tsx draws the table. The skeleton stays until DataTables fires its init event.

Example

Pages in the App Router are server components: they render on the server and cannot pass functions to the browser. data, columns, options and the <thead> are plain values and markup, so a page can render DataTable directly. Leave title out of columns. The <thead> holds the titles, and DataTables would write over it.

src/app/(dashboard)/employees/page.tsx
import { DataTable } from "@/components/plugins/datatable";
import { pageMetadata } from "@/lib/site";

export const metadata = pageMetadata("/employees");

const employees = [
  { name: "Tiger Nixon", position: "System Architect", office: "Edinburgh", age: 61, startDate: "2026/09/12", salary: "$320,800" },
  { name: "Garrett Winters", position: "Accountant", office: "Tokyo", age: 63, startDate: "2026/09/25", salary: "$170,750" },
  { name: "Ashton Cox", position: "Junior Technical Author", office: "San Francisco", age: 66, startDate: "2026/01/21", salary: "$86,000" },
  { name: "Cedric Kelly", position: "Senior Javascript Developer", office: "Edinburgh", age: 22, startDate: "2026/07/15", salary: "$433,060" },
  { name: "Airi Satou", position: "Accountant", office: "Tokyo", age: 33, startDate: "2026/01/14", salary: "$162,700" },
  { name: "Brielle Williamson", position: "Integration Specialist", office: "New York", age: 61, startDate: "2026/08/21", salary: "$372,000" },
  { name: "Herrod Chandler", position: "Sales Assistant", office: "San Francisco", age: 59, startDate: "2026/08/03", salary: "$137,500" },
  { name: "Rhona Davidson", position: "Integration Specialist", office: "Tokyo", age: 55, startDate: "2026/04/27", salary: "$327,900" },
  { name: "Colleen Hurst", position: "Javascript Developer", office: "San Francisco", age: 39, startDate: "2026/02/27", salary: "$205,500" },
  { name: "Sonya Frost", position: "Software Engineer", office: "Edinburgh", age: 23, startDate: "2026/01/17", salary: "$103,600" },
  { name: "Jena Gaines", position: "Office Manager", office: "London", age: 30, startDate: "2026/01/18", salary: "$90,560" },
  { name: "Quinn Flynn", position: "Support Lead", office: "Edinburgh", age: 22, startDate: "2026/09/04", salary: "$342,000" },
];

const columns = [
  { data: "name" },
  { data: "position" },
  { data: "office" },
  { data: "age" },
  { data: "startDate" },
  { data: "salary" },
];

export default function EmployeesPage() {
  return (
    <div className="card">
      <div className="card-body">
        <DataTable
          id="datatables-employees"
          className="table table-striped"
          data={employees}
          columns={columns}
          options={{ responsive: true }}
        >
          <thead>
            <tr>
              <th>Name</th>
              <th>Position</th>
              <th>Office</th>
              <th>Age</th>
              <th>Start date</th>
              <th>Salary</th>
            </tr>
          </thead>
        </DataTable>
      </div>
    </div>
  );
}

pageMetadata() reads the title and description of the route from src/data/pages.json. When you add a new route such as /employees, add it there too.

Props

The props are those of datatables.net-react:

PropTypeDefaultDescription
dataany[]The rows, as objects or arrays
columnsobject[]DataTables' column options, such as data
ajaxstring | objectA URL or Ajax configuration to load the rows from
optionsDataTableOptionsAny other DataTables option
classNamestringClasses for the <table>, such as table table-striped
idstringid of the <table>
childrenReactNodeThe <thead>, and Column elements for custom cells
refRef<DataTableRef>ref.current.dt() returns the DataTables API
on<Event>functionA DataTables event in camelCase, such as onInit or onSelect

A server component cannot hand a function to a client component. So as soon as you pass on<Event>, a ref, a render function in columns or a Column with a function child, move the table into a file that starts with "use client".

The DataTableOptions and DataTableApi types come from src/components/plugins/datatable-helpers.ts. The defaults keep two things from DataTables 1: the "Show 10 entries" length menu and the Previous and Next pager.

Extensions

Each extension has an option that turns it on:

OptionExtensionDemo
responsive: trueHides columns that do not fit and shows them in a row belowResponsive
layout: { topStart: { buttons: ["copy", "print"] } }Copy and print buttons above the tableButtons
fixedHeader: trueKeeps the header in view while the page scrollsFixed header
select: { style: "multi" }A click selects or deselects a rowMulti selection

The types of the Buttons extension leave out the layout feature, so the Buttons demo page casts its options:

import type { DataTableOptions } from "@/components/plugins/datatable-helpers";

const options = {
  responsive: true,
  layout: { topStart: { buttons: ["copy", "print"] } },
} as DataTableOptions;

Custom cells

When a cell needs a badge or other JSX, render it with a Column child. A Column takes a function, so the table has to live in a client component, as src/components/pages/apps/orders/orders-table.tsx does. Wrap the function in rowCell when it needs the whole row. The column keeps its data, which sorting and searching still use:

src/components/orders-table.tsx
"use client";

import { Column, DataTable } from "@/components/plugins/datatable";
import { rowCell } from "@/components/plugins/datatable-helpers";
import { type Order, orders } from "@/mocks/orders";

export function OrdersTable() {
  return (
    <DataTable className="table table-striped" data={orders} options={{ responsive: true }}>
      <thead>
        <tr>
          <th>#</th>
          <th>Total</th>
          <th>Status</th>
        </tr>
      </thead>
      <Column data="id">{(id: string) => <strong>{id}</strong>}</Column>
      <Column data="total">
        {rowCell((order: Order) =>
          order.status === "Paid" ? order.total : <s>{order.total}</s>
        )}
      </Column>
      <Column data="status">
        {(status: string) => <span className="badge badge-success-light">{status}</span>}
      </Column>
    </DataTable>
  );
}

Ajax data

The app builds to static files (output: "export"), so there is no route handler to serve rows while it runs. Point ajax at a JSON file in public/ or at your own API instead. The browser fetches it when the table starts, and asset() from @/lib/site adds the base path. The Ajax demo reads public/tables-datatables-ajax.json from a server component page. Its rows are arrays in column order:

import { asset } from "@/lib/site";

<DataTable
  className="table table-striped"
  ajax={asset("/tables-datatables-ajax.json")}
  columns={[
    { footer: "Name" },
    { footer: "Position" },
    { footer: "Office" },
    { footer: "Age" },
    { footer: "Start date" },
    { footer: "Salary" },
  ]}
>
  <thead>...</thead>
</DataTable>

The DataTables API

To filter or redraw the table from your own code, you need the DataTables API, and so a client component. onInit gets the API as event.dt once the table has its data. Later on, a ref gives it to you through ref.current?.dt():

<DataTable
  data={employees}
  columns={columns}
  onInit={(event: { dt: DataTableApi }) => {
    event.dt.column(2).search("Tokyo").draw();
  }}
>

Accessibility

  • Keep a <thead> with a <th> per column, so screen readers announce the column of each cell.
  • DataTables labels its own search field and page length select. Label the controls you add yourself, such as column filters, with a <label> or aria-label.
  • The skeleton has aria-hidden="true", so assistive technology skips it until the table is there.

Last updated on

On this page