Skip to content
Drivn
5 min read

How to Add a Table to a React App

Step-by-step guide to add a data table to any React app with the Drivn CLI — a copy-paste, server-safe Table with striped and bordered variants.

Reaching for a table means your React app has rows of structured data to show — an orders list, a pricing matrix, a team roster, an invoice summary. You could hand-write <table>, <thead>, and <tbody> with their border, padding, and alignment classes every time, but you would re-solve the same layout on each screen. The Table in Drivn packages it into one small file you paste into your project — a compound set of semantic wrappers with built-in striped and bordered variants and a variant prop to switch between them.

Drivn installs by copy-paste: the CLI writes a table.tsx file into your repository. The component is pure presentation — it holds no state and carries no 'use client' directive, so it renders anywhere React reaches the DOM, including straight inside a Server Component where you can await your data first. Its only import past React is the local cn utility; there is no headless layer and no icon dependency, because a table is just semantic HTML with Tailwind classes over <thead>, <tbody>, <tr>, <th>, and <td>.

This guide adds the Table to any React app — install the CLI, render your first table, switch variants, and align columns with a caption and footer. For the App Router walkthrough see the Next.js Table guide, and for copy-paste layouts see the Table examples.

Prerequisites

Before installing Table, confirm your React project has the two things Drivn assumes: Tailwind CSS v4 installed and processing your styles, and a @/ path alias pointing at your source directory — the component ships as a .tsx file, so TypeScript is part of the stack. If you scaffolded with create-next-app, npm create vite, or npx drivn@latest create, both are already wired; for a custom setup the installation page lists the minimal config. The Table has no third-party import at all past React and the local cn utility — there is no headless table library and no icon package to add. The one detail worth knowing before you render it is the opposite of most interactive components: the Table has no 'use client' directive, so it renders in a Server Component with no client boundary and no hydration cost for the table itself.

Step 1 — Install Drivn via the CLI

Run the CLI from your project root to add the Table source file. The command prompts once for your install directory (defaulting to src/components/ui/) and writes a single table.tsx — no other files, because the component's only dependency past React is the cn utility. No global config file is created; the result is TypeScript source you edit like any other component. Confirm it landed in your editor, then commit it for a clean baseline. If your project uses a monorepo layout or a non-standard source path, the CLI docs cover the flags for targeting a custom location during install. Because the file is yours after install, future Drivn releases will not overwrite your edits — the whole component, from the styles object to every semantic wrapper, is one file you can read top to bottom.

1# add the Table source file
2npx drivn add table

Step 2 — Render your first table

Import Table and compose the parts to mirror the HTML you already know: Table.Header wraps a header Table.Row of Table.Head cells, and Table.Body wraps the data rows of Table.Cell cells. One import { Table } brings the whole set — the sub-components are attached to the root via Object.assign, so you write Table.Header and Table.Cell rather than importing seven separate names. Because the Table is server-safe, you can map rows from data you fetched higher up with no client boundary; the example renders a small static roster. The root also wraps the native <table> in an overflow-x-auto div, so a wide table scrolls sideways on small screens instead of breaking the layout. For the full prop list see the Table reference.

1import { Table } from '@/components/ui/table'
2
3export function TeamTable() {
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}

Step 3 — Switch to striped or bordered rows

The variant prop on the Table root changes the look with no change to your row markup. The default is an unadorned table with a 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 you render. Pass variant="bordered" and every <th> and <td> gains a border-border box plus the same even-row tint for a spreadsheet-style grid. Because the striping is a structural nth-child selector rather than a per-row class, you never tag individual Table.Row elements. For a heavier 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>
Get started

Install Drivn in one command

Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.

Follow Drivn updates
New components, improvements, and guides every release.
Enjoying Drivn?
Star the repo on GitHub to follow new component releases.
Star →

Frequently asked questions

No. The Table is pure presentational markup — it has no React hooks and no event handlers, so its source carries no 'use client' directive and renders straight inside a 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 with no hydration cost. Only an interactive wrapper, like a client-side sort control, needs its own client boundary — the Table primitives never do.

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 you render — add or remove rows and the pattern re-flows automatically with no code change on your side.

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 alignment.

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. Set the project up with TypeScript and Tailwind v4 first, then run npx drivn add table and compose the same Table.Header, Table.Body, and Table.Row parts. Outside the App Router there is no Server Component boundary to think about at all.