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.
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":
"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.
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:
| Prop | Type | Default | Description |
|---|---|---|---|
data | any[] | The rows, as objects or arrays | |
columns | object[] | DataTables' column options, such as data | |
ajax | string | object | A URL or Ajax configuration to load the rows from | |
options | DataTableOptions | Any other DataTables option | |
className | string | Classes for the <table>, such as table table-striped | |
id | string | id of the <table> | |
children | ReactNode | The <thead>, and Column elements for custom cells | |
ref | Ref<DataTableRef> | ref.current.dt() returns the DataTables API | |
on<Event> | function | A 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:
| Option | Extension | Demo |
|---|---|---|
responsive: true | Hides columns that do not fit and shows them in a row below | Responsive |
layout: { topStart: { buttons: ["copy", "print"] } } | Copy and print buttons above the table | Buttons |
fixedHeader: true | Keeps the header in view while the page scrolls | Fixed header |
select: { style: "multi" } | A click selects or deselects a row | Multi 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:
"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>oraria-label. - The skeleton has
aria-hidden="true", so assistive technology skips it until the table is there.
Related
- React DataTables: the same component in AdminKit for React (Vite)
- Bootstrap 5 DataTables: the same plugin in the HTML version
- Tables: Bootstrap's table classes, no client component needed
- DataTables demo
Last updated on
Layout
Next.js Bootstrap form layout for the App Router: grid, horizontal and inline forms and input groups as server-rendered markup, plus a client Dropdown.
CalendarPro
Next.js FullCalendar for the App Router: FullCalendar 7 loaded on the client with next/dynamic, with month, week and day views and drag and drop.