Next.js Data Table for the App Router
Add a sortable, selectable data table to a Next.js App Router app. Drivn ships a single-file Data Table built on plain React with zero runtime deps.
A data table is the workhorse of every admin panel, dashboard, and internal tool — the sortable, selectable grid that turns an array of records into something a user can scan and act on. Building one in Next.js means wiring column sorting, row selection with an indeterminate header checkbox, a loading skeleton, an empty state, and pagination, then keeping all of it accessible. Most teams pull in a heavyweight grid library and inherit a large API surface for what is often a few hundred rows.
Drivn takes the copy-and-own route instead. After npx drivn add data-table the source lands in src/components/ui/data-table.tsx, marked 'use client', composing four other Drivn primitives — Table, Checkbox, Skeleton, and Pagination — with lucide-react for the sort arrows as its only icon dependency. The API is a generic DataTable component plus a DataTable.Pagination subcomponent attached with Object.assign, so one import covers both. You pass data, a columns array of ColumnDef objects, and a rowKey, and the component handles the rest.
This guide installs Drivn in a Next.js 16 project, renders the table as a client island, enables column sorting and row selection, and wires custom cell renderers, pagination, and the loading state. Every snippet maps to the component's actual source. For the full reference see the Data Table docs; for the shadcn/ui comparison see Drivn vs shadcn/ui Data Table.
Install in a Next.js 16 project
Drivn installs through a small CLI that writes the component source straight into your repository — there is no runtime grid package to version-lock. Open a terminal at the root of your Next.js 16 project and run npx drivn add data-table. The CLI prompts once for your install directory (defaulting to src/components/ui/), copies data-table.tsx, and resolves the internal dependencies it composes — Table, Checkbox, Skeleton, and Pagination — installing each of those files alongside it. It also adds lucide-react to your package.json for the ArrowUp, ArrowDown, and ArrowUpDown sort icons. The CLI reference documents every flag, including targeting a custom path. After install you own the files outright — future Drivn releases never overwrite them. Commit them first to keep a clean baseline before customizing.
1 # from the root of your Next.js 16 project 2 npx drivn add data-table
Render the table as a client island
An App Router page is a server component by default, but the Data Table holds sort and selection state with useState and runs a useMemo to sort rows, so its source is marked 'use client'. The clean pattern is to keep the table in a small client component and render that island inside an otherwise server-rendered page — fetch your rows on the server, pass them down as a prop. Import DataTable from @/components/ui/data-table, define a columns array where each entry has an id matching a key on your row and a header label, then pass data, columns, and a rowKey that names the unique property on each row. The installation guide covers project bootstrapping.
1 'use client' 2 import { DataTable } from '@/components/ui/data-table' 3 4 const columns = [ 5 { id: 'name', header: 'Name' }, 6 { id: 'email', header: 'Email' }, 7 { id: 'role', header: 'Role' }, 8 ] 9 10 export function UsersTable({ users }: { users: User[] }) { 11 return ( 12 <DataTable 13 data={users} 14 columns={columns} 15 rowKey="id" 16 /> 17 ) 18 }
Add sorting and row selection
Pass the sortable prop to make every column sortable, or set sortable on an individual ColumnDef to override per column. Clicking a header cycles the sort through ascending, descending, then off — the component tracks this in a sort state object and re-sorts inside a useMemo, using a numeric subtraction for number columns and localeCompare for strings. Add selectable to render a checkbox column: the header checkbox toggles all rows, and the component drives its indeterminate state through a ref so it shows a partial mark when some but not all rows are checked. Selection is stored as a Set of row keys and surfaced through onSelectionChange. See the Data Table examples for the full set of patterns.
1 'use client' 2 import { useState } from 'react' 3 import { DataTable } from '@/components/ui/data-table' 4 5 export function UsersTable({ users }: { users: User[] }) { 6 const [selected, setSelected] = useState<Set<string>>(new Set()) 7 return ( 8 <DataTable 9 data={users} 10 columns={columns} 11 rowKey="id" 12 sortable 13 selectable 14 onSelectionChange={setSelected} 15 /> 16 ) 17 }
Custom cells, pagination, and loading
A ColumnDef can take a cell renderer — a (value, row) => ReactNode function — to format a value into a badge, link, or any JSX instead of plain text. For paging, slice your data and render DataTable.Pagination through the footer prop; it shows the current page and wires Previous and Next buttons through the Drivn Pagination primitive. While data loads, pass loading to swap the body for five Skeleton rows, and when data is empty the table renders a built-in "No results." row automatically — no extra markup. Because the whole file lives in your codebase, restyling is a source edit: open data-table.tsx, find the const styles object, and adjust the keys. See the Data Table docs for every prop.
1 <DataTable 2 data={users} 3 columns={[ 4 { id: 'name', header: 'Name' }, 5 { 6 id: 'status', 7 header: 'Status', 8 cell: (value) => ( 9 <Badge variant={value === 'Active' ? 'default' : 'secondary'}> 10 {String(value)} 11 </Badge> 12 ), 13 }, 14 ]} 15 rowKey="id" 16 loading={isLoading} 17 />
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
npx drivn@latest createRequires Node 18+. Works with npm, pnpm, and yarn.
Frequently asked questions
The Data Table itself is a client component — it is marked 'use client' because it tracks sort and selection state with useState and sorts rows inside a useMemo. Your surrounding page and layout still render on the server. The usual pattern is to fetch rows in a server component, then pass them to a small client component holding the table, rendered as an island inside an otherwise server-rendered route.
Set the sortable prop on the table to enable it for all columns, or on a single ColumnDef for one column. Clicking a header cycles ascending, descending, and off. The component holds the active sort in state and recomputes the order in a useMemo, comparing number columns with subtraction and string columns with localeCompare. An onSortChange callback fires with the new sort if you need to sort on the server instead.
No. The Data Table is built from plain React and Tailwind, composing four Drivn primitives — Table, Checkbox, Skeleton, and Pagination. Sorting, selection, the loading skeleton, and the empty state are all handled in the component's own code. The only runtime dependency it adds is lucide-react for the sort arrow icons. For very large datasets with virtualization you would reach for a dedicated grid; for typical admin and dashboard tables this stays lighter and fully yours to edit.
Slice your data to the current page yourself, then render DataTable.Pagination through the table's footer prop. The subcomponent takes page, pageCount, and onPageChange, renders the "Page X of Y" label, and wires Previous and Next buttons through the Drivn Pagination primitive. Because you control the slicing, the same pattern works for client-side paging or server-side paging where each page is a fresh fetch.

