Skip to content
Drivn
5 min read

Drivn vs shadcn/ui — Table Compared

A data Table is one of the few shadcn components with no Radix under it — so this comparison is about API shape, built-in variants, and how the source reads.

A data Table is one of the few components where shadcn/ui does not reach for Radix — both Drivn and shadcn render plain semantic <table> elements with no headless primitive underneath. That makes this comparison less about bundle size and more about API ergonomics: how you import the pieces, how you switch visual styles, and how the source reads when you open it. Drivn ships a single file at packages/drivn/src/registry/components/table.ts that exports one Table object with seven sub-components attached via Object.assignTable.Header, Table.Body, Table.Row, Table.Head, Table.Cell, Table.Footer, and Table.Caption.

shadcn/ui splits the same surface into seven separately named exports — Table, TableHeader, TableBody, TableRow, TableHead, TableCell, TableCaption — each its own component with inline cn() calls. You import all seven by name and compose them flat. Neither approach pulls a runtime UI dependency; both lean only on a cn() class-merge helper, and both render server-side with no 'use client' directive.

The practical differences are the dot-notation single import, Drivn's built-in variant prop for striped and bordered styles, and the const styles object that keeps every class in one place. This page walks each difference with verbatim source, then covers where an existing shadcn project should stay put.

Side-by-side comparison

FeatureDrivnshadcn/ui
Install commandnpx drivn add tablenpx shadcn@latest add table
Runtime UI dependenciescn() utility only — zero npm UI packagescn() utility only — Table is not Radix-backed
API shapeDot notation — Table.Header, Table.CellNamed exports — TableHeader, TableCell
Import statementOne name: import { Table }Up to seven names imported flat
Built-in variantsdefault / striped / bordered via variant propNone — add classes manually
Striped rowsvariant="striped"Manual [&_tr:nth-child(even)] classes
Cell alignmentalign prop → CSS attribute selectorclassName="text-center" per cell
Styling patternSingle const styles objectInline cn() per sub-component
Client componentNo — renders pure server-safe markupNo — renders pure server-safe markup
LicenseMITMIT

One import, dot notation — Table.Header not TableHeader

Drivn attaches every table part to the root via Object.assign(TableRoot, { Caption, Header, Body, Footer, Row, Head, Cell }), so a single import { Table } from "@/components/ui/table" gives you the whole set. You write Table.Header, Table.Row, Table.Head, and Table.Cell — the structure reads like the HTML it renders, and your import block stays one line no matter how complex the table grows.

shadcn/ui exports each part separately, so the same table needs import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "@/components/ui/table" — six or seven names to keep in sync as you add a caption or footer. The flat naming is a matter of taste, but the dot notation means one import line and a component tree that mirrors <thead> / <tbody> / <tr> / <th> / <td> without the Table prefix repeated on every tag. See the full API on the Table reference.

1import { Table } from "@/components/ui/table"
2
3export 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}

Built-in striped and bordered variants

Drivn bakes three visual styles into a variant prop — default, striped, and bordered — typed as keyof typeof styles.variants so your editor autocompletes the three options and rejects typos. Switching a plain table to zebra striping is <Table variant="striped">; adding cell borders is <Table variant="bordered">. The classes that drive each variant live in the styles.variants object, applied to the root <table> and cascading to tbody rows via Tailwind arbitrary selectors.

shadcn/ui ships no variant prop on its table. To stripe rows you add an [&_tr:nth-child(even)] background utility to the TableBody className yourself; to border cells you append border utilities to every TableHead and TableCell. It is the same Tailwind under the hood, but Drivn pre-packages the common cases behind one type-safe prop so you do not re-derive the arbitrary selectors for each table you build.

1// packages/drivn/src/registry/components/table.ts — verbatim
2variants: {
3 default: '',
4 striped: '[&_tbody_tr:nth-child(even)]:bg-muted/30',
5 bordered: cn(
6 '[&_th]:border [&_td]:border',
7 '[&_th]:border-border [&_td]:border-border',
8 '[&_tbody_tr:nth-child(even)]:bg-muted/30'
9 ),
10},

Cell alignment via a native attribute selector

Both Table.Head and Table.Cell accept an align prop, but Drivn wires it through the native HTML align attribute rather than a className. The styles.head and styles.cell entries include [&[align=center]]:text-center and [&[align=right]]:text-right — Tailwind arbitrary attribute selectors that match the rendered align="center" or align="right" on the <th> / <td>. So <Table.Cell align="center">$29</Table.Cell> centres the cell with zero extra classes, and the alignment travels with the semantic attribute.

shadcn/ui leaves alignment to you — you pass className="text-center" on each TableCell you want centred. The rendered output is identical, but Drivn's attribute-driven approach keeps the alignment declarative and out of the className soup, which matters when a pricing or comparison table has a dozen right-aligned number columns. The verbatim head and cell style entries show how the selectors map onto the attribute.

1// packages/drivn/src/registry/components/table.ts — verbatim
2head: cn(
3 'px-4 py-2.5 text-left font-semibold',
4 'text-muted-foreground',
5 '[&[align=center]]:text-center',
6 '[&[align=right]]:text-right'
7),
8cell: cn(
9 'px-4 py-2.5 text-foreground',
10 '[&[align=center]]:text-center',
11 '[&[align=right]]:text-right'
12),

When an existing shadcn table should stay put

The honest case for shadcn/ui here is momentum and the TanStack Table ecosystem. shadcn's docs pair its table primitives with @tanstack/react-table for sorting, filtering, pagination, and column visibility — a data table recipe that thousands of projects have copied verbatim. If your table is already that TanStack-driven shadcn data table, rewriting it onto Drivn buys you dot notation and a variant prop but none of the headless logic, so the migration is rarely worth it.

The second case is a codebase already standardised on shadcn's flat naming — switching one component to dot notation means two conventions living in the same @/components/ui folder. For a fresh build, though, where you want one import, built-in striped and bordered styles, and a const styles object you can read top to bottom, the Drivn Table is the cleaner start. Pair it with Badge for status pills and Pagination for page controls and you have a full data surface without a single runtime UI dependency.

1# Drivn one import, built-in variants
2npx drivn add table
3
4# shadcn/ui seven named exports, no variant prop
5npx shadcn@latest add table
Get started

Install Drivn in one command

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

npx drivn@latest create

Requires Node 18+. Works with npm, pnpm, and yarn.

Enjoying Drivn?
Star the repo on GitHub to follow new component releases.
Star →

Frequently asked questions

No. The Drivn Table is a single file at packages/drivn/src/registry/components/table.ts that imports React and the local cn() utility from @/utils/cn — nothing else. It renders plain semantic table, thead, tbody, tr, th, and td elements. Notably, shadcn/ui's Table is also dependency-free here — the table is one of the few shadcn components that does not wrap a Radix primitive, so neither library adds a runtime UI package for this component.

Drivn uses dot notation — one import of { Table } gives you Table.Header, Table.Body, Table.Row, Table.Head, Table.Cell, Table.Footer, and Table.Caption attached to the root via Object.assign. shadcn/ui exports each part as a separate name — TableHeader, TableBody, TableRow, and so on — which you import flat. Same rendered markup, but Drivn keeps the import to one line and the tree mirrors the underlying HTML structure.

Yes. 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. There is also variant="bordered" for cell borders plus striping. shadcn/ui has no variant prop, so the same zebra effect there means adding the nth-child arbitrary selector to the TableBody className by hand each time.

Yes. The Table source has no 'use client' directive and no React hooks — it is pure presentational markup, so it renders on the server with zero client JavaScript for the table itself. You only need a client component when you add interactivity like column sorting, and even then only the interactive wrapper needs 'use client', not the Table primitives.

Pass align="right" on the Table.Head and Table.Cell for that column. Drivn forwards it as the native HTML align attribute, and the head and cell style entries include [&[align=right]]:text-right, a Tailwind attribute selector that right-aligns the text. This keeps alignment declarative and tied to the semantic attribute rather than scattered className="text-right" strings on every numeric cell.

Yes. Because the Drivn Table is plain markup with no internal state, you can drive it with @tanstack/react-table the same way shadcn does — call useReactTable for the headless model, then map getHeaderGroups and getRowModel onto Table.Header, Table.Row, Table.Head, and Table.Cell. Drivn gives you the presentation layer and dot-notation API; TanStack supplies the sorting, filtering, and pagination logic on top.