Next.js Table with the App Router
Render a data table in a Next.js App Router app. Drivn ships a copy-paste, zero-dependency Table with striped and bordered variants — no client boundary.
A table is where structured data lands — an orders list, a pricing matrix, a comparison grid. The detail worth knowing up front in a Next.js App Router project is that Drivn's Table is a pure presentational component with no 'use client' directive, so it renders straight inside an async Server Component. You can await a database query or a fetch at the top of the route, map the rows into Table.Row children, and ship the whole thing as server-rendered HTML — no client boundary, no hydration cost for the table itself.
After install the component sits in src/components/ui/table.tsx as a compound set: a Table root plus Table.Header, Table.Body, Table.Row, Table.Head, Table.Cell, Table.Caption, and Table.Footer. The root wraps a native <table> in an overflow-x-auto div so wide tables scroll on small screens, and it takes a variant prop — default, striped, or bordered — to switch the look without touching the markup. Under the hood every part is a thin wrapper over the matching HTML element with cn-merged Tailwind classes, so it stays semantic <thead>, <tbody>, <tr>, <th>, and <td> for accessibility and print.
This guide installs Drivn in a Next.js 16 project, renders a Table from server-fetched data, switches variants, aligns columns and adds a caption and footer, then restyles from the file you own. For the full reference see the Table docs; for the shadcn/ui comparison see Drivn vs shadcn/ui Table.
Install in a Next.js 16 project
Drivn installs through a small CLI that writes component source directly into your repository — there is no runtime package to version-lock. From the root of your Next.js 16 project run npx drivn add table. The Table imports nothing past React and the local cn utility — no icon dependency, no headless layer — so the CLI writes a single table.tsx file and touches nothing else in your tree. The CLI reference documents every flag, including targeting a custom directory or installing several components at once. After install you own the file, and future Drivn releases will not overwrite it, so commit it for a clean baseline before wiring it into a page. There is no peer dependency to add and nothing to configure past having Tailwind v4 already processing your styles — the component is pure React plus Tailwind classes over semantic table elements.
1 # from the root of your Next.js 16 project 2 npx drivn add table
Render a table in a Server Component
Because table.tsx has no 'use client' directive, it renders straight inside an async Server Component — the piece that sets the Table apart from most interactive UI. Fetch your rows at the top of the route with await, then map them into Table.Row children inside Table.Body; the whole table serializes to HTML on the server with no hydration for the table itself. The root wraps the native <table> in an overflow-x-auto div, Table.Header renders <thead>, Table.Body renders <tbody>, Table.Head renders a <th scope="col">, and Table.Cell renders a <td>. The example awaits a list of orders and renders one row each. Keep the data-fetching in the Server Component and the Table stays server-rendered end to end — see the installation guide for project setup.
1 import { Table } from '@/components/ui/table' 2 3 async function getOrders() { 4 const res = await fetch('https://api.example.com/orders', { 5 next: { revalidate: 60 }, 6 }) 7 return res.json() as Promise< 8 { id: string; customer: string; total: string }[] 9 > 10 } 11 12 export default async function OrdersPage() { 13 const orders = await getOrders() 14 15 return ( 16 <Table> 17 <Table.Header> 18 <Table.Row> 19 <Table.Head>Order</Table.Head> 20 <Table.Head>Customer</Table.Head> 21 <Table.Head align="right">Total</Table.Head> 22 </Table.Row> 23 </Table.Header> 24 <Table.Body> 25 {orders.map((order) => ( 26 <Table.Row key={order.id}> 27 <Table.Cell>{order.id}</Table.Cell> 28 <Table.Cell>{order.customer}</Table.Cell> 29 <Table.Cell align="right">{order.total}</Table.Cell> 30 </Table.Row> 31 ))} 32 </Table.Body> 33 </Table> 34 ) 35 }
Switch variants: striped and bordered
The variant prop on the Table root changes the look without any change to the row markup. The default variant is an unadorned table with a bottom border under the header and a divider under each row. Pass variant="striped" and even-numbered body rows pick up a bg-muted/30 tint via a [&_tbody_tr:nth-child(even)] selector — the zebra striping renders purely from CSS, so it stays correct no matter how many rows the server returns. Pass variant="bordered" and every <th> and <td> gains a border-border box plus the same even-row tint, giving a spreadsheet-style grid. Because the striping is a nth-child selector rather than a per-row class, you never tag individual Table.Row elements — the variant on the root does all the work. For a heavier data grid with sorting and pagination, reach for the Data Table instead.
1 <Table variant="striped"> 2 <Table.Header> 3 <Table.Row> 4 <Table.Head>Plan</Table.Head> 5 <Table.Head align="right">Price</Table.Head> 6 </Table.Row> 7 </Table.Header> 8 <Table.Body> 9 <Table.Row> 10 <Table.Cell>Starter</Table.Cell> 11 <Table.Cell align="right">$0</Table.Cell> 12 </Table.Row> 13 <Table.Row> 14 <Table.Cell>Pro</Table.Cell> 15 <Table.Cell align="right">$12</Table.Cell> 16 </Table.Row> 17 </Table.Body> 18 </Table>
Restyle from the styles object
Everything visible on the Table is a Tailwind class in the styles object inside the file you own, so restyling is a local edit. The header uses [&_tr]:border-b [&_tr]:border-border, each row is border-b border-border transition-colors hover:bg-muted/40 for a hover highlight, and cells are px-4 py-2.5. Tighten the density by dropping the cell padding to px-3 py-1.5, or change the hover tint from bg-muted/40 to a brand token for a stronger row highlight. Because the classes read your theme tokens, the table matches dark and light mode with no override — see Theming for the token list. The styles keys below are copied verbatim from the registry source; edit them and every Table in your app follows.
1 // packages/drivn/src/registry/components/table.ts — verbatim 2 header: '[&_tr]:border-b [&_tr]:border-border', 3 body: '[&_tr:last-child]:border-0', 4 footer: 'border-t border-border bg-muted/30 font-medium', 5 row: 'border-b border-border transition-colors hover:bg-muted/40', 6 head: cn( 7 'px-4 py-2.5 text-left font-semibold', 8 'text-muted-foreground', 9 '[&[align=center]]:text-center', 10 '[&[align=right]]:text-right' 11 ), 12 cell: cn( 13 'px-4 py-2.5 text-foreground', 14 '[&[align=center]]:text-center', 15 '[&[align=right]]:text-right' 16 ),
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
Yes. The Table has no 'use client' directive — it is a pure presentational wrapper over the native table elements — so it renders straight inside an async Server Component. Fetch your rows at the top of the route with await, map them into Table.Row children, and the whole table serializes to HTML on the server with no hydration cost for the table itself. That is the opposite of most interactive Drivn components, which need a client boundary.
Three, set through the variant prop on the Table root: default renders an unadorned table with header and row borders, striped tints even-numbered body rows with bg-muted/30, and bordered adds a border-border box around every th and td plus the same even-row tint for a spreadsheet-style grid. The variant lives on the root, so you switch the whole table look with one prop and never tag individual rows.
The striped variant applies a [&_tbody_tr:nth-child(even)]:bg-muted/30 selector on the table root, so the browser tints every even body row directly from CSS. Because it is a structural nth-child selector rather than a class on each Table.Row, the striping stays correct no matter how many rows the server returns — add or remove rows and the pattern re-flows automatically with no code change.
Pass align="right" to the Table.Head and each Table.Cell in that column. The head and cell style keys include [&[align=right]]:text-right and [&[align=center]]:text-center hooks that read the native align attribute, so setting align on the element applies the alignment with no extra className. Numeric columns like prices and totals read best right-aligned; leave text columns at the default left.
Yes. The component depends only on React and the local cn utility — no Next.js API and no router — so it works anywhere React and Tailwind reach the DOM. In a Vite + React app there is no Server Component boundary to think about; render the Table with client-fetched data the same way. Run npx drivn add table and compose the same Table.Header, Table.Body, and Table.Row parts.

