How to Add a Data Table to a React App
Step-by-step guide to adding a copy-and-own Data Table to any React project with the Drivn CLI — column sorting, row selection, loading, and pagination.
A data table is more than an HTML <table> — it sorts columns, selects rows with checkboxes, shows a loading skeleton, paginates, and stays readable on every screen. Build that from scratch and you are juggling sort state, a selection Set, an indeterminate header checkbox, and pagination math before you render a single row. Most teams reach for a headless table library and spend a day wiring its render props.
Drivn writes the whole thing into your repository instead. The CLI copies a data-table.tsx file that composes Drivn's own Table, Checkbox, Skeleton, and Pagination into one typed DataTable component — column sorting, row selection, loading states, and an empty message all built in. After install the file is yours: read it end to end, restyle the styles object, extend the ColumnDef type. Because it tracks sort and selection with useState, it is a client component marked 'use client', while the page around it can still render on the server.
This guide adds the Data Table to an existing React app in about fifteen minutes — install the CLI, define columns, render rows, turn on sorting and selection, then paginate. It works the same in Vite + React or a Next.js App Router project. For the framework-specific walkthrough see the Next.js Data Table guide; for live patterns see the Data Table examples.
Prerequisites
Before installing the Data Table, confirm your React project has the three things Drivn assumes: Tailwind CSS v4 installed and processing your CSS, TypeScript configured (the component ships as a .tsx file and is generic over your row type), and a @/ path alias pointing at your source directory. If you scaffolded with create-next-app, npm create vite, or npx drivn@latest create, all three are already wired. For a custom setup, check the compilerOptions.paths entry in tsconfig.json; the installation page lists the minimal config. The Data Table composes four Drivn primitives — Table, Checkbox, Skeleton, and Pagination — and the CLI resolves and writes each one during install, along with adding lucide-react for the sort icons if it is missing.
Step 1 — Install Drivn via the CLI
Run the CLI from your project root to add the Data Table source file. The command prompts once for your install directory (defaulting to src/components/ui/), writes data-table.tsx, and resolves its component dependencies by also writing table.tsx, checkbox.tsx, skeleton.tsx, and pagination.tsx, since the Data Table imports all four directly. It adds lucide-react to your package.json if it is not already present — that is the only third-party package the table needs, used for the sort-direction arrows. No global config file is created — every file is TypeScript you edit like any other component. Confirm they landed in your editor, then commit them. If your project uses a monorepo layout or a non-standard path, the CLI docs cover the flags for targeting a custom location.
1 # add the Data Table (the CLI also writes its four dependencies) 2 npx drivn add data-table 3 4 # verify the files were written 5 ls src/components/ui/data-table.tsx src/components/ui/table.tsx
Step 2 — Define columns and render rows
The Data Table is data-driven: you pass a data array, a columns array of ColumnDef objects, and a rowKey naming the unique field on each row. Each column needs an id matching a key on your row and a header string; the table reads row[col.id] for the cell value and stringifies it by default. The rowKey tells the component which field is the stable React key and the selection identifier, so it must be unique across rows. That is the entire required surface — three props render a clean, sortable-ready table. Import it from your UI directory and pass plain arrays; see the Data Table docs for the full prop table.
1 import { useState } from "react" 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 const users = [ 11 { id: "1", name: "Alice Johnson", email: "alice@example.com", role: "Admin", status: "Active" }, 12 { id: "2", name: "Bob Smith", email: "bob@example.com", role: "Editor", status: "Active" }, 13 { id: "3", name: "Charlie Brown", email: "charlie@example.com", role: "Viewer", status: "Inactive" }, 14 ] 15 16 export default function Page() { 17 return ( 18 <DataTable 19 data={users} 20 columns={columns} 21 rowKey="id" 22 /> 23 ) 24 }
Step 3 — Enable sorting, selection, and custom cells
Three opt-in props cover the common interactions. Pass sortable to make every column header a sort toggle, or set sortable: true on individual ColumnDef objects for per-column control; clicking a header cycles ascending, descending, then back to the original order, and numeric columns sort numerically while everything else sorts by localeCompare. Pass selectable to add a checkbox column with an indeterminate header checkbox, and read the chosen rows through onSelectionChange, which hands you a Set of the selected rowKey values. To render anything other than plain text in a cell — a Badge, a link, a formatted date — give the column a cell function that receives the value and the row and returns a ReactNode. Use align to right-align numbers.
1 import { Badge } from "@/components/ui/badge" 2 3 const columns = [ 4 { id: "name", header: "Name", sortable: true }, 5 { id: "email", header: "Email" }, 6 { 7 id: "status", 8 header: "Status", 9 cell: (value) => <Badge>{String(value)}</Badge>, 10 }, 11 ] 12 13 <DataTable 14 data={users} 15 columns={columns} 16 rowKey="id" 17 selectable 18 onSelectionChange={(keys) => console.log([...keys])} 19 />
Step 4 — Loading state, empty state, and pagination
Three more props handle the states a real table needs. Pass loading while data is in flight and the body renders five skeleton rows shaped to your columns, so the layout never jumps. When data is empty the table shows a centered "No results." message automatically — no conditional rendering on your side. For pagination, slice your data yourself and render DataTable.Pagination in the footer prop; it is the same component exposed through dot notation, and it takes page, pageCount, and onPageChange. Keeping pagination controlled means it works identically against client arrays or server-fetched pages. See the Data Table examples for filtering and column-visibility patterns built on these same props.
1 import { useState } from "react" 2 import { DataTable } from "@/components/ui/data-table" 3 4 const pageSize = 3 5 const [page, setPage] = useState(1) 6 const pageData = users.slice((page - 1) * pageSize, page * pageSize) 7 const pageCount = Math.ceil(users.length / pageSize) 8 9 <DataTable 10 data={pageData} 11 columns={columns} 12 rowKey="id" 13 footer={ 14 <DataTable.Pagination 15 page={page} 16 pageCount={pageCount} 17 onPageChange={setPage} 18 /> 19 } 20 />
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
Frequently asked questions
Yes. Set the project up with TypeScript and Tailwind v4 first, then run npx drivn add data-table. The Data Table has no dependency on Next.js or any router — it renders anywhere React and Tailwind reach the DOM. Vite + React is the most common non-Next setup and works without extra configuration; the CLI also writes the Table, Checkbox, Skeleton, and Pagination components the Data Table composes.
The Data Table is a client component — it is marked 'use client' because it tracks sort order and the selected-row Set with useState. Your surrounding page and layout still render on the server. The usual pattern is to fetch data in a server component and pass it as a plain array prop into a client component that renders the table, so data loading stays on the server while the interactive table hydrates as an island.
Sorting is client-side and built in. Enable it with the sortable prop on the table or on individual columns, and clicking a header cycles through ascending, descending, and the original unsorted order. The component sorts a memoized copy of your data — numeric values compare numerically and everything else uses localeCompare. An onSortChange callback fires with the active column id and direction if you need to mirror the sort to a server query instead.
Give the column a cell function in its ColumnDef. It receives the raw cell value and the full row object and returns any ReactNode, so you can wrap the value in a Badge, render a link, format a date, or compose an actions button. Columns without a cell function stringify their value automatically. The align property on the column right- or center-aligns the cell content for numbers and status chips.
Pagination ships as DataTable.Pagination, exposed through dot notation on the same import. You slice the data for the current page yourself and render the component in the footer prop with page, pageCount, and onPageChange. Keeping it controlled means the exact same markup paginates a client-side array or a server-fetched page — you decide where the slicing happens. The component itself wraps Drivn's Pagination primitive.

