React Table Component Examples
Copy-paste React Table examples: a basic data table, striped and bordered variants, a pricing table with footer, status badges in cells, and column sorting.
A data table is the workhorse of any dashboard, admin panel, or settings screen — a grid of rows and columns that has to stay readable as the data grows. The Table in Drivn is a set of seven dot-notation sub-components attached to one root via Object.assign: Table.Header, Table.Body, Table.Row, Table.Head, Table.Cell, Table.Footer, and Table.Caption. It renders plain semantic <table> markup wrapped in an overflow-x-auto div, carries no runtime UI dependency beyond cn() from @/utils/cn, and ships with no 'use client' directive — so a static table is a server component with zero client JavaScript.
Every example below imports Table from @/components/ui/table — the path the Drivn CLI installs it under — and every snippet is TypeScript because Drivn ships TypeScript-only .tsx source. The static examples render on the server; only the sorting example reaches for 'use client' and React.useState, and even then only the wrapper needs it, not the Table primitives.
The examples cover a basic data table, the built-in striped and bordered variants, a pricing table that uses Table.Caption, Table.Footer, and centered columns, a status column built with Badge, and a fully sortable table driven by React.useState.
A basic data table
The starting point is a Table with a Table.Header row of Table.Head cells and a Table.Body of Table.Row / Table.Cell pairs. The header row gets a bottom border from the styles.header entry ([&_tr]:border-b), each body row carries border-b border-border plus a hover:bg-muted/40 highlight, and the last body row drops its border via [&_tr:last-child]:border-0. The whole table sits inside a w-full overflow-x-auto wrapper so narrow viewports scroll horizontally instead of breaking the layout.
Because nothing here is interactive, this is a pure server component — no 'use client', no hooks, no client bundle cost for the table. Drop it straight into a page or a server-rendered Card. The dot notation keeps the import to a single import { Table } line, and the component tree reads top to bottom like the <thead> / <tbody> structure it produces.
1 import { Table } from "@/components/ui/table" 2 3 export default function Page() { 4 return ( 5 <Table> 6 <Table.Header> 7 <Table.Row> 8 <Table.Head>Name</Table.Head> 9 <Table.Head>Role</Table.Head> 10 <Table.Head>Status</Table.Head> 11 </Table.Row> 12 </Table.Header> 13 <Table.Body> 14 <Table.Row> 15 <Table.Cell>Alice</Table.Cell> 16 <Table.Cell>Engineer</Table.Cell> 17 <Table.Cell>Active</Table.Cell> 18 </Table.Row> 19 <Table.Row> 20 <Table.Cell>Bob</Table.Cell> 21 <Table.Cell>Designer</Table.Cell> 22 <Table.Cell>Active</Table.Cell> 23 </Table.Row> 24 </Table.Body> 25 </Table> 26 ) 27 }
Striped and bordered variants
For long lists, zebra striping makes rows easier to track across columns. Pass variant="striped" on the Table root and the styles.variants.striped entry applies [&_tbody_tr:nth-child(even)]:bg-muted/30, tinting every even body row. For dense data grids where you want a cell on every boundary, variant="bordered" adds [&_th]:border [&_td]:border with border-border plus the same even-row striping. The variant prop is typed as keyof typeof styles.variants, so your editor autocompletes default, striped, and bordered and rejects anything else.
Neither variant requires extra classes on the rows or cells — the arbitrary Tailwind selectors live in the root's styles.variants object and cascade down. Swap a plain table to striped by changing one prop, no markup churn. This is the example to reach for on a transactions list, a user directory, or any table long enough that row tracking becomes a usability problem.
1 import { Table } from "@/components/ui/table" 2 3 export default function StripedTable() { 4 return ( 5 <Table variant="striped"> 6 <Table.Header> 7 <Table.Row> 8 <Table.Head>Name</Table.Head> 9 <Table.Head>Role</Table.Head> 10 <Table.Head>Status</Table.Head> 11 </Table.Row> 12 </Table.Header> 13 <Table.Body> 14 <Table.Row> 15 <Table.Cell>Alice</Table.Cell> 16 <Table.Cell>Engineer</Table.Cell> 17 <Table.Cell>Active</Table.Cell> 18 </Table.Row> 19 <Table.Row> 20 <Table.Cell>Bob</Table.Cell> 21 <Table.Cell>Designer</Table.Cell> 22 <Table.Cell>Active</Table.Cell> 23 </Table.Row> 24 <Table.Row> 25 <Table.Cell>Charlie</Table.Cell> 26 <Table.Cell>PM</Table.Cell> 27 <Table.Cell>Away</Table.Cell> 28 </Table.Row> 29 </Table.Body> 30 </Table> 31 ) 32 }
A status column built with Badge
Raw status text like "Active" or "Failed" is hard to scan in a busy table. Render a Badge inside the status Table.Cell instead, and the color does the scanning for you. The Badge variant prop maps cleanly onto states — success for active, outline for idle or away, destructive for failed or blocked. Keep the row data in an array and map it to Table.Row elements so the table stays declarative as the dataset changes.
This pattern is the backbone of any operational dashboard — deploys, jobs, users, invoices — where a glance should tell you which rows need attention. The Table primitives stay server-renderable; the Badge is equally static, so the whole table is still a server component with no client JavaScript. Add a Pagination control underneath once the list outgrows one screen.
1 import { Table } from "@/components/ui/table" 2 import { Badge } from "@/components/ui/badge" 3 4 const rows = [ 5 { name: "Alice", role: "Engineer", status: "Active" }, 6 { name: "Bob", role: "Designer", status: "Away" }, 7 { name: "Charlie", role: "PM", status: "Failed" }, 8 ] 9 10 const variants = { 11 Active: "success", 12 Away: "outline", 13 Failed: "destructive", 14 } as const 15 16 export default function StatusTable() { 17 return ( 18 <Table> 19 <Table.Header> 20 <Table.Row> 21 <Table.Head>Name</Table.Head> 22 <Table.Head>Role</Table.Head> 23 <Table.Head>Status</Table.Head> 24 </Table.Row> 25 </Table.Header> 26 <Table.Body> 27 {rows.map((row) => ( 28 <Table.Row key={row.name}> 29 <Table.Cell>{row.name}</Table.Cell> 30 <Table.Cell>{row.role}</Table.Cell> 31 <Table.Cell> 32 <Badge variant={variants[row.status]}> 33 {row.status} 34 </Badge> 35 </Table.Cell> 36 </Table.Row> 37 ))} 38 </Table.Body> 39 </Table> 40 ) 41 }
A sortable table with useState
Sorting is where the table turns into a client component. Hold the rows and a sort descriptor in React.useState, derive a sorted copy with Array.prototype.sort, and toggle the direction when a Table.Head is clicked. Because the Drivn Table is plain markup with no internal state, the sorting logic lives entirely in your wrapper — you own the comparator, the direction toggle, and the click handler, and the Table just renders whatever order you hand it.
This stays lightweight for small to mid-size datasets sorted in the browser. For server-side sorting, large datasets, or column filtering and pagination, lift the same idea into @tanstack/react-table and map its row model onto the Table primitives — that is exactly what the data table recipe does. For a sortable directory or a leaderboard that fits in memory, the useState approach below is all you need.
1 "use client" 2 3 import * as React from "react" 4 import { Table } from "@/components/ui/table" 5 6 type Row = { name: string; score: number } 7 8 const data: Row[] = [ 9 { name: "Alice", score: 92 }, 10 { name: "Bob", score: 78 }, 11 { name: "Charlie", score: 85 }, 12 ] 13 14 export default function SortableTable() { 15 const [asc, setAsc] = React.useState(true) 16 17 const sorted = [...data].sort((a, b) => 18 asc ? a.score - b.score : b.score - a.score 19 ) 20 21 return ( 22 <Table> 23 <Table.Header> 24 <Table.Row> 25 <Table.Head>Name</Table.Head> 26 <Table.Head 27 align="right" 28 onClick={() => setAsc((v) => !v)} 29 className="cursor-pointer select-none" 30 > 31 Score {asc ? "↑" : "↓"} 32 </Table.Head> 33 </Table.Row> 34 </Table.Header> 35 <Table.Body> 36 {sorted.map((row) => ( 37 <Table.Row key={row.name}> 38 <Table.Cell>{row.name}</Table.Cell> 39 <Table.Cell align="right">{row.score}</Table.Cell> 40 </Table.Row> 41 ))} 42 </Table.Body> 43 </Table> 44 ) 45 }
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
Pass variant="striped" on the Table root. The styles.variants.striped entry applies [&_tbody_tr:nth-child(even)]:bg-muted/30, which tints every even row in the body. There is no per-row className needed — the arbitrary Tailwind selector lives on the root table and cascades down. For bordered cells plus striping, use variant="bordered" instead, which adds border utilities to every th and td as well.
Static tables are server components. The Table source has no 'use client' directive and no hooks, so it renders as pure semantic markup with zero client JavaScript. You only opt into a client component when you add interactivity like column sorting — and even then only the wrapper that owns the useState needs 'use client', not the Table.Header, Table.Row, or Table.Cell primitives themselves.
Pass align="right" on both the Table.Head and the Table.Cell for that column. Drivn forwards align as the native HTML attribute, and the head and cell style entries include [&[align=right]]:text-right, a Tailwind attribute selector that right-aligns the text. This keeps numbers visually aligned without adding className="text-right" to every cell in the column.
Yes. Render a Badge component inside the status Table.Cell and map the row status to a Badge variant — success for active, outline for idle, destructive for failed. Both Table and Badge are dependency-free and server-renderable, so the whole table stays a server component. Keep the row data in an array and map it to Table.Row elements so the markup stays declarative.
Hold the rows and a sort direction in React.useState, derive a sorted copy with Array.prototype.sort, and toggle the direction from a Table.Head onClick handler. The Table itself has no internal state, so you own the comparator and just render the sorted order. Mark the wrapper 'use client' for the useState. For large datasets, lift the logic into @tanstack/react-table and map its row model onto the Table primitives.
Yes. The Table root wraps its table element in a div with w-full overflow-x-auto, so on narrow viewports the table scrolls horizontally instead of overflowing or wrapping its columns. You do not need to add a scroll container yourself — it is built into the wrapper. For very wide tables, this keeps the page layout intact while letting users swipe across the columns.

