React table for displaying structured data with sorting and accessibility support. Built with TypeScript and Tailwind CSS for Next.js.
You know how tables can either make your data crystal clear or turn into an unreadable mess? Ever tried building a responsive table that doesn't break on mobile? Or handled a client asking "can we just make it work like Excel?" That's the reality of data display in modern web apps. This shadcn/ui table gives you the foundation to build tables that actually work—from simple invoices to complex data grids in your React applications.
Clean, structured data presentation:
"use client" import { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, } from "~/components/ui/table" const invoices = [ { invoice: "INV001", paymentStatus: "Paid", totalAmount: "$250.00", paymentMethod: "Credit Card", }, { invoice: "INV002", paymentStatus: "Pending", totalAmount: "$150.00", paymentMethod: "PayPal", }, { invoice: "INV003", paymentStatus: "Unpaid", totalAmount: "$350.00", paymentMethod: "Bank Transfer", }, { invoice: "INV004", paymentStatus: "Paid", totalAmount: "$450.00", paymentMethod: "Credit Card", }, { invoice: "INV005", paymentStatus: "Paid", totalAmount: "$550.00", paymentMethod: "PayPal", }, { invoice: "INV006", paymentStatus: "Pending", totalAmount: "$200.00", paymentMethod: "Bank Transfer", }, { invoice: "INV007", paymentStatus: "Unpaid", totalAmount: "$300.00", paymentMethod: "Credit Card", }, ] export default function TableDemo() { return ( <div className="w-full max-w-4xl mx-auto p-6"> <Table> <TableCaption>A list of your recent invoices.</TableCaption> <TableHeader> <TableRow> <TableHead className="w-[100px]">Invoice</TableHead> <TableHead>Status</TableHead> <TableHead>Method</TableHead> <TableHead className="text-right">Amount</TableHead> </TableRow> </TableHeader> <TableBody> {invoices.map(invoice => ( <TableRow key={invoice.invoice}> <TableCell className="font-medium">{invoice.invoice}</TableCell> <TableCell>{invoice.paymentStatus}</TableCell> <TableCell>{invoice.paymentMethod}</TableCell> <TableCell className="text-right">{invoice.totalAmount}</TableCell> </TableRow> ))} </TableBody> <TableFooter> <TableRow> <TableCell colSpan={3}>Total</TableCell> <TableCell className="text-right">$2,500.00</TableCell> </TableRow> </TableFooter> </Table> </div> ) }
Simple semantic HTML table with proper structure—caption, header, body, and footer. This free open source React component gives you all the building blocks for accessible data presentation. Built with TypeScript for full type safety while styled with Tailwind CSS to match your design system.
npx shadcn@latest add table
Here's the thing—everyone wants to reinvent tables with cards, lists, or fancy grids. But when you need to compare data across multiple dimensions, nothing beats a well-structured table. Users know how to read them. Screen readers understand them. They print properly. They export to CSV naturally.
Think about your banking app showing transactions. A spreadsheet displaying sales data. Your project management tool listing tasks. Tables work because they show relationships—this row relates to this column at this intersection. That's powerful for data comprehension.
This free shadcn table handles the foundation—semantic HTML, accessibility, responsive design. Whether you're building admin dashboards, financial reports, or inventory systems in your Next.js applications, tables that respect user expectations while looking modern keep your JavaScript projects professional.
Structured payment data with totals:
"use client" import { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, } from "~/components/ui/table" const invoices = [ { invoice: "INV001", paymentStatus: "Paid", totalAmount: "$250.00", paymentMethod: "Credit Card", }, { invoice: "INV002", paymentStatus: "Pending", totalAmount: "$150.00", paymentMethod: "PayPal", }, { invoice: "INV003", paymentStatus: "Unpaid", totalAmount: "$350.00", paymentMethod: "Bank Transfer", }, { invoice: "INV004", paymentStatus: "Paid", totalAmount: "$450.00", paymentMethod: "Credit Card", }, { invoice: "INV005", paymentStatus: "Paid", totalAmount: "$550.00", paymentMethod: "PayPal", }, { invoice: "INV006", paymentStatus: "Pending", totalAmount: "$200.00", paymentMethod: "Bank Transfer", }, { invoice: "INV007", paymentStatus: "Unpaid", totalAmount: "$300.00", paymentMethod: "Credit Card", }, ] export default function TableDemo() { return ( <div className="w-full max-w-4xl mx-auto p-6"> <Table> <TableCaption>A list of your recent invoices.</TableCaption> <TableHeader> <TableRow> <TableHead className="w-[100px]">Invoice</TableHead> <TableHead>Status</TableHead> <TableHead>Method</TableHead> <TableHead className="text-right">Amount</TableHead> </TableRow> </TableHeader> <TableBody> {invoices.map(invoice => ( <TableRow key={invoice.invoice}> <TableCell className="font-medium">{invoice.invoice}</TableCell> <TableCell>{invoice.paymentStatus}</TableCell> <TableCell>{invoice.paymentMethod}</TableCell> <TableCell className="text-right">{invoice.totalAmount}</TableCell> </TableRow> ))} </TableBody> <TableFooter> <TableRow> <TableCell colSpan={3}>Total</TableCell> <TableCell className="text-right">$2,500.00</TableCell> </TableRow> </TableFooter> </Table> </div> ) }
Admin tables with actions and status:
"use client" import { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, } from "~/components/ui/table" const invoices = [ { invoice: "INV001", paymentStatus: "Paid", totalAmount: "$250.00", paymentMethod: "Credit Card", }, { invoice: "INV002", paymentStatus: "Pending", totalAmount: "$150.00", paymentMethod: "PayPal", }, { invoice: "INV003", paymentStatus: "Unpaid", totalAmount: "$350.00", paymentMethod: "Bank Transfer", }, { invoice: "INV004", paymentStatus: "Paid", totalAmount: "$450.00", paymentMethod: "Credit Card", }, { invoice: "INV005", paymentStatus: "Paid", totalAmount: "$550.00", paymentMethod: "PayPal", }, { invoice: "INV006", paymentStatus: "Pending", totalAmount: "$200.00", paymentMethod: "Bank Transfer", }, { invoice: "INV007", paymentStatus: "Unpaid", totalAmount: "$300.00", paymentMethod: "Credit Card", }, ] export default function TableDemo() { return ( <div className="w-full max-w-4xl mx-auto p-6"> <Table> <TableCaption>A list of your recent invoices.</TableCaption> <TableHeader> <TableRow> <TableHead className="w-[100px]">Invoice</TableHead> <TableHead>Status</TableHead> <TableHead>Method</TableHead> <TableHead className="text-right">Amount</TableHead> </TableRow> </TableHeader> <TableBody> {invoices.map(invoice => ( <TableRow key={invoice.invoice}> <TableCell className="font-medium">{invoice.invoice}</TableCell> <TableCell>{invoice.paymentStatus}</TableCell> <TableCell>{invoice.paymentMethod}</TableCell> <TableCell className="text-right">{invoice.totalAmount}</TableCell> </TableRow> ))} </TableBody> <TableFooter> <TableRow> <TableCell colSpan={3}>Total</TableCell> <TableCell className="text-right">$2,500.00</TableCell> </TableRow> </TableFooter> </Table> </div> ) }
Interactive table with column operations:
"use client" import { type ColumnDef, type ColumnFiltersState, flexRender, getCoreRowModel, getFilteredRowModel, getSortedRowModel, type SortingState, useReactTable, type VisibilityState, } from "@tanstack/react-table" import { ArrowUpDown, ChevronDown, MoreHorizontal } from "lucide-react" import * as React from "react" import { Badge } from "~/components/ui/badge" import { Button } from "~/components/ui/button" import { Checkbox } from "~/components/ui/checkbox" import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "~/components/ui/dropdown-menu" import { Input } from "~/components/ui/input" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "~/components/ui/table" interface Payment { id: string amount: number status: "pending" | "processing" | "success" | "failed" email: string } const data: Payment[] = [ { id: "m5gr84i9", amount: 316, status: "success", email: "[email protected] " }, { id: "3u1reuv4", amount: 242, status: "success", email: "[email protected] " }, { id: "derv1ws0", amount: 837, status: "processing", email: "[email protected] " }, { id: "5kma53ae", amount: 874, status: "success", email: "[email protected] " }, { id: "bhqecj4p", amount: 721, status: "failed", email: "[email protected] " }, ] const columns: ColumnDef<Payment>[] = [ { id: "select", header: ({ table }) => ( <Checkbox aria-label="Select all" checked={ table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate") } onCheckedChange={value => table.toggleAllPageRowsSelected(!!value)} /> ), cell: ({ row }) => ( <Checkbox aria-label="Select row" checked={row.getIsSelected()} onCheckedChange={value => row.toggleSelected(!!value)} /> ), enableSorting: false, enableHiding: false, }, { accessorKey: "id", header: "Transaction", cell: ({ row }) => <div className="font-medium">{row.getValue("id")}</div>, }, { accessorKey: "email", header: ({ column }) => { return ( <Button onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} variant="ghost" > Email <ArrowUpDown className="ml-2 h-4 w-4" /> </Button> ) }, cell: ({ row }) => <div className="lowercase">{row.getValue("email")}</div>, }, { accessorKey: "status", header: "Status", cell: ({ row }) => { const status = row.getValue("status") as string return ( <Badge variant={ status === "success" ? "default" : status === "processing" ? "secondary" : status === "pending" ? "outline" : "destructive" } > {status} </Badge> ) }, }, { accessorKey: "amount", header: ({ column }) => { return ( <div className="text-right"> <Button onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} variant="ghost" > Amount <ArrowUpDown className="ml-2 h-4 w-4" /> </Button> </div> ) }, cell: ({ row }) => { const amount = Number.parseFloat(row.getValue("amount")) const formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", }).format(amount) return <div className="text-right font-medium">{formatted}</div> }, }, { id: "actions", enableHiding: false, cell: ({ row }) => { const payment = row.original return ( <DropdownMenu> <DropdownMenuTrigger asChild> <Button className="h-8 w-8 p-0" variant="ghost"> <span className="sr-only">Open menu</span> <MoreHorizontal className="h-4 w-4" /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuLabel>Actions</DropdownMenuLabel> <DropdownMenuItem onClick={() => navigator.clipboard.writeText(payment.id)}> Copy payment ID </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem>View customer</DropdownMenuItem> <DropdownMenuItem>View payment details</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> ) }, }, ] export default function TableData() { const [sorting, setSorting] = React.useState<SortingState>([]) const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]) const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({}) const [rowSelection, setRowSelection] = React.useState({}) const table = useReactTable({ data, columns, onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), onColumnVisibilityChange: setColumnVisibility, onRowSelectionChange: setRowSelection, state: { sorting, columnFilters, columnVisibility, rowSelection, }, }) return ( <div className="w-full max-w-4xl mx-auto p-6"> <div className="flex items-center py-4"> <Input className="max-w-sm" onChange={event => table.getColumn("email")?.setFilterValue(event.target.value)} placeholder="Filter emails..." value={(table.getColumn("email")?.getFilterValue() as string) ?? ""} /> <DropdownMenu> <DropdownMenuTrigger asChild> <Button className="ml-auto" variant="outline"> Columns <ChevronDown className="ml-2 h-4 w-4" /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> {table .getAllColumns() .filter(column => column.getCanHide()) .map(column => { return ( <DropdownMenuCheckboxItem checked={column.getIsVisible()} className="capitalize" key={column.id} onCheckedChange={value => column.toggleVisibility(!!value)} > {column.id} </DropdownMenuCheckboxItem> ) })} </DropdownMenuContent> </DropdownMenu> </div> <div className="rounded-md border"> <Table> <TableHeader> {table.getHeaderGroups().map(headerGroup => ( <TableRow key={headerGroup.id}> {headerGroup.headers.map(header => { return ( <TableHead key={header.id}> {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} </TableHead> ) })} </TableRow> ))} </TableHeader> <TableBody> {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map(row => ( <TableRow data-state={row.getIsSelected() && "selected"} key={row.id}> {row.getVisibleCells().map(cell => ( <TableCell key={cell.id}> {flexRender(cell.column.columnDef.cell, cell.getContext())} </TableCell> ))} </TableRow> )) ) : ( <TableRow> <TableCell className="h-24 text-center" colSpan={columns.length}> No results. </TableCell> </TableRow> )} </TableBody> </Table> </div> <div className="flex items-center justify-between pt-4"> <div className="text-sm text-muted-foreground"> {table.getFilteredSelectedRowModel().rows.length} of{" "} {table.getFilteredRowModel().rows.length} row(s) selected. </div> </div> </div> ) }
Tables that transform gracefully on small screens:
"use client" import { Badge } from "~/components/ui/badge" import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card" import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow, } from "~/components/ui/table" const orders = [ { id: "ORD001", customer: "John Doe", product: "MacBook Pro", status: "shipped", date: "2024-01-15", amount: "$2,499.00", }, { id: "ORD002", customer: "Sarah Wilson", product: "iPhone 15", status: "processing", date: "2024-01-16", amount: "$999.00", }, { id: "ORD003", customer: "Mike Johnson", product: "AirPods Pro", status: "delivered", date: "2024-01-14", amount: "$249.00", }, { id: "ORD004", customer: "Emma Brown", product: "iPad Air", status: "pending", date: "2024-01-17", amount: "$599.00", }, { id: "ORD005", customer: "David Lee", product: "Apple Watch", status: "shipped", date: "2024-01-13", amount: "$399.00", }, ] function getStatusBadgeVariant(status: string) { switch (status) { case "delivered": return "default" case "shipped": return "secondary" case "processing": return "outline" case "pending": return "destructive" default: return "outline" } } export default function TableResponsive() { return ( <div className="w-full max-w-4xl mx-auto p-6"> {/* Desktop Table */} <div className="hidden md:block"> <Table> <TableCaption>Recent orders from your store.</TableCaption> <TableHeader> <TableRow> <TableHead className="w-[100px]">Order</TableHead> <TableHead>Customer</TableHead> <TableHead>Product</TableHead> <TableHead>Status</TableHead> <TableHead>Date</TableHead> <TableHead className="text-right">Amount</TableHead> </TableRow> </TableHeader> <TableBody> {orders.map(order => ( <TableRow key={order.id}> <TableCell className="font-medium">{order.id}</TableCell> <TableCell>{order.customer}</TableCell> <TableCell>{order.product}</TableCell> <TableCell> <Badge variant={getStatusBadgeVariant(order.status)}>{order.status}</Badge> </TableCell> <TableCell>{order.date}</TableCell> <TableCell className="text-right">{order.amount}</TableCell> </TableRow> ))} </TableBody> </Table> </div> {/* Mobile Cards */} <div className="md:hidden space-y-4"> {orders.map(order => ( <Card key={order.id}> <CardHeader className="pb-3"> <CardTitle className="flex items-center justify-between text-base"> <span>{order.id}</span> <Badge variant={getStatusBadgeVariant(order.status)}>{order.status}</Badge> </CardTitle> </CardHeader> <CardContent className="space-y-2"> <div className="flex justify-between"> <span className="text-sm text-muted-foreground">Customer</span> <span className="text-sm font-medium">{order.customer}</span> </div> <div className="flex justify-between"> <span className="text-sm text-muted-foreground">Product</span> <span className="text-sm font-medium">{order.product}</span> </div> <div className="flex justify-between"> <span className="text-sm text-muted-foreground">Date</span> <span className="text-sm font-medium">{order.date}</span> </div> <div className="flex justify-between pt-1 border-t"> <span className="text-sm text-muted-foreground">Amount</span> <span className="text-sm font-semibold">{order.amount}</span> </div> </CardContent> </Card> ))} </div> </div> ) }
Performance data with visual indicators:
"use client" import { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, } from "~/components/ui/table" const invoices = [ { invoice: "INV001", paymentStatus: "Paid", totalAmount: "$250.00", paymentMethod: "Credit Card", }, { invoice: "INV002", paymentStatus: "Pending", totalAmount: "$150.00", paymentMethod: "PayPal", }, { invoice: "INV003", paymentStatus: "Unpaid", totalAmount: "$350.00", paymentMethod: "Bank Transfer", }, { invoice: "INV004", paymentStatus: "Paid", totalAmount: "$450.00", paymentMethod: "Credit Card", }, { invoice: "INV005", paymentStatus: "Paid", totalAmount: "$550.00", paymentMethod: "PayPal", }, { invoice: "INV006", paymentStatus: "Pending", totalAmount: "$200.00", paymentMethod: "Bank Transfer", }, { invoice: "INV007", paymentStatus: "Unpaid", totalAmount: "$300.00", paymentMethod: "Credit Card", }, ] export default function TableDemo() { return ( <div className="w-full max-w-4xl mx-auto p-6"> <Table> <TableCaption>A list of your recent invoices.</TableCaption> <TableHeader> <TableRow> <TableHead className="w-[100px]">Invoice</TableHead> <TableHead>Status</TableHead> <TableHead>Method</TableHead> <TableHead className="text-right">Amount</TableHead> </TableRow> </TableHeader> <TableBody> {invoices.map(invoice => ( <TableRow key={invoice.invoice}> <TableCell className="font-medium">{invoice.invoice}</TableCell> <TableCell>{invoice.paymentStatus}</TableCell> <TableCell>{invoice.paymentMethod}</TableCell> <TableCell className="text-right">{invoice.totalAmount}</TableCell> </TableRow> ))} </TableBody> <TableFooter> <TableRow> <TableCell colSpan={3}>Total</TableCell> <TableCell className="text-right">$2,500.00</TableCell> </TableRow> </TableFooter> </Table> </div> ) }
These patterns show tables handling everything from financial data to user management. Each example demonstrates how this open source component adapts to different data types while maintaining clarity and usability.
This free open source table component includes everything you need:
TypeScript-first - Full type safety for table data and column definitions in React
Semantic HTML - Proper table elements that screen readers understand naturally
Responsive ready - Transform to cards or scroll horizontally on mobile devices
Tailwind CSS styled - Customize with utilities, not fighting table-specific CSS
TanStack compatible - Upgrade to advanced features when you need them
Accessibility built-in - Caption, headers, proper structure for assistive tech
Print friendly - Tables that actually look good on paper (remember paper?)
Export ready - Semantic structure makes CSV/Excel export straightforward
Prop Type Purpose classNamestring Tailwind CSS classes for styling childrenReactNode Table content and structure Standard HTML attributes various All native table element props
Feature How It Helps Semantic HTML Screen readers understand structure naturally TableCaption Describes table purpose for context Header associations Links data cells to column headers Keyboard navigation Tab through interactive elements
Always include a TableCaption even if visually hidden. This free shadcn/ui table component supports captions natively—use them to describe what data the table contains. Screen reader users need context before diving into rows and columns. "Invoice items for Order #12345" beats no caption.
Handle empty states with helpful messages instead of blank tables. When there's no data, show users why and what they can do about it. This open source shadcn table is just the structure—you provide meaningful empty states that guide users to add their first items or adjust filters.
Consider mobile layouts before defaulting to horizontal scroll. Tables get cramped on phones. This TypeScript component can transform into cards or lists with Tailwind CSS breakpoints. Test with real devices—sometimes a complete layout change serves mobile users better than a scrollable table.
Use consistent alignment for similar data types across tables. Numbers right, text left, currencies with proper formatting. This React component handles the display—you establish the data patterns. When users see multiple tables in your Next.js application, consistent alignment reduces cognitive load.
Test with extreme data to find edge cases early. Long email addresses, missing values, numbers with many digits, text in different languages. The JavaScript component handles rendering—you ensure the layout doesn't break. Better to find issues during development than in production.
Tables naturally pair with Pagination components for large datasets in your React applications. Use Badge components for status indicators and Button components for row actions like edit or delete.
For advanced features, combine tables with DataTable components that add sorting, filtering, and column visibility. This open source pattern provides powerful data management while maintaining clean structure.
When building forms, use tables with Checkbox components for row selection or Input components for inline editing. Dropdown Menu components work great for row action menus that don't clutter the interface.
For loading states, pair tables with Skeleton components to show structure while data loads. ScrollArea components help with fixed-height tables. Your JavaScript application can compose these shadcn components for complete data experiences.