# 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](/pricing/#nextjs).

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](https://datatables.net/) to the pages of [AdminKit for Next.js](/nextjs/). 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":

```tsx title="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.

```html
<table id="datatables-example" class="table table-striped" style="width:100%">
  <thead>
    <tr><th>Name</th><th>Position</th><th>Office</th><th>Age</th><th>Start date</th><th>Salary</th></tr>
  </thead>
  <tbody>
    <tr><td>Tiger Nixon</td><td>System Architect</td><td>Edinburgh</td><td>61</td><td>2026/09/12</td><td>$320,800</td></tr>
    <tr><td>Garrett Winters</td><td>Accountant</td><td>Tokyo</td><td>63</td><td>2026/09/25</td><td>$170,750</td></tr>
    <tr><td>Ashton Cox</td><td>Junior Technical Author</td><td>San Francisco</td><td>66</td><td>2026/01/21</td><td>$86,000</td></tr>
    <tr><td>Cedric Kelly</td><td>Senior Javascript Developer</td><td>Edinburgh</td><td>22</td><td>2026/07/15</td><td>$433,060</td></tr>
    <tr><td>Airi Satou</td><td>Accountant</td><td>Tokyo</td><td>33</td><td>2026/01/14</td><td>$162,700</td></tr>
    <tr><td>Brielle Williamson</td><td>Integration Specialist</td><td>New York</td><td>61</td><td>2026/08/21</td><td>$372,000</td></tr>
    <tr><td>Herrod Chandler</td><td>Sales Assistant</td><td>San Francisco</td><td>59</td><td>2026/08/03</td><td>$137,500</td></tr>
    <tr><td>Rhona Davidson</td><td>Integration Specialist</td><td>Tokyo</td><td>55</td><td>2026/04/27</td><td>$327,900</td></tr>
    <tr><td>Colleen Hurst</td><td>Javascript Developer</td><td>San Francisco</td><td>39</td><td>2026/02/27</td><td>$205,500</td></tr>
    <tr><td>Sonya Frost</td><td>Software Engineer</td><td>Edinburgh</td><td>23</td><td>2026/01/17</td><td>$103,600</td></tr>
    <tr><td>Jena Gaines</td><td>Office Manager</td><td>London</td><td>30</td><td>2026/01/18</td><td>$90,560</td></tr>
    <tr><td>Quinn Flynn</td><td>Support Lead</td><td>Edinburgh</td><td>22</td><td>2026/09/04</td><td>$342,000</td></tr>
  </tbody>
</table>

<script>
new DataTable("#datatables-example", { responsive: true });
</script>
```
```tsx code title="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`:

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `data` | `any[]` | | The rows, as objects or arrays |
| `columns` | `object[]` | | DataTables' [column options](https://datatables.net/reference/option/columns), such as `data` |
| `ajax` | `string \| object` | | A URL or Ajax configuration to load the rows from |
| `options` | `DataTableOptions` | | Any other [DataTables option](https://datatables.net/reference/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](https://nextjs-demo.adminkit.io/tables-datatables-responsive) |
| `layout: { topStart: { buttons: ["copy", "print"] } }` | Copy and print buttons above the table | [Buttons](https://nextjs-demo.adminkit.io/tables-datatables-buttons) |
| `fixedHeader: true` | Keeps the header in view while the page scrolls | [Fixed header](https://nextjs-demo.adminkit.io/tables-datatables-fixed-header) |
| `select: { style: "multi" }` | A click selects or deselects a row | [Multi selection](https://nextjs-demo.adminkit.io/tables-datatables-multi) |

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

```tsx
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:

```tsx title="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:

```tsx
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()`:

```tsx
<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.

## Related

- [React DataTables](/react/docs/plugins/data-table/): the same component in AdminKit for React (Vite)
- [Bootstrap 5 DataTables](/bootstrap/docs/plugins/data-table/): the same plugin in the HTML version
- [Tables](/nextjs/docs/components/tables/): Bootstrap's table classes, no client component needed
- [DataTables demo](https://nextjs-demo.adminkit.io/tables-datatables-responsive)

---

Canonical HTML page: https://adminkit.io/nextjs/docs/plugins/data-table/

Site overview for agents: https://adminkit.io/llms.txt (expanded: https://adminkit.io/llms-full.txt) · Documentation: https://adminkit.io/bootstrap/docs/ · Questions: support@adminkit.io
