Docs
Table

Table

A responsive table component for presenting data inline.

Table is an opinionated <table> element for basic tabular data presentation. It is usually presented within a Card.

Loading...
import {
  Card,
  Table,
  TableBody,
  TableCaption,
  TableCell,
  TableFooter,
  TableHead,
  TableHeader,
  TableRow,
} from 'ui'
 
const invoices = [
  {
    invoice: 'INV001',
    paymentStatus: 'Paid',
    totalAmount: '$250.00',
    paymentMethod: 'Credit card',
    description: 'Website design services',
  },
  {
    invoice: 'INV002',
    paymentStatus: 'Pending',
    totalAmount: '$150.00',
    paymentMethod: 'PayPal',
    description: 'Monthly subscription fee',
  },
  {
    invoice: 'INV003',
    paymentStatus: 'Unpaid',
    totalAmount: '$350.00',
    paymentMethod: 'Bank transfer',
    description: 'Consulting hours',
  },
  {
    invoice: 'INV004',
    paymentStatus: 'Paid',
    totalAmount: '$450.00',
    paymentMethod: 'Credit card',
    description: 'Software license renewal',
  },
  {
    invoice: 'INV005',
    paymentStatus: 'Paid',
    totalAmount: '$550.00',
    paymentMethod: 'PayPal',
    description: 'Custom development work',
  },
  {
    invoice: 'INV006',
    paymentStatus: 'Pending',
    totalAmount: '$200.00',
    paymentMethod: 'Bank transfer',
    description: 'Hosting and maintenance',
  },
  {
    invoice: 'INV007',
    paymentStatus: 'Unpaid',
    totalAmount: '$300.00',
    paymentMethod: 'Credit card',
    description: 'Training session package',
  },
]
 
export function TableDemo() {
  return (
    <Card className="w-full">
      <Table>
        <TableCaption className="border-0">A list of your recent invoices</TableCaption>
        <TableHeader>
          <TableRow>
            <TableHead>Invoice</TableHead>
            <TableHead>Status</TableHead>
            <TableHead>Method</TableHead>
            <TableHead className="hidden md:table-cell">Description</TableHead>
            <TableHead className="text-right">Amount</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {invoices.map((invoice) => (
            <TableRow key={invoice.invoice}>
              <TableCell className="text-foreground font-mono">{invoice.invoice}</TableCell>
              <TableCell className="text-foreground-lighter">{invoice.paymentStatus}</TableCell>
              <TableCell className="text-foreground-lighter">{invoice.paymentMethod}</TableCell>
              <TableCell className="hidden md:table-cell text-foreground-muted">
                {invoice.description}
              </TableCell>
              <TableCell className="text-right">{invoice.totalAmount}</TableCell>
            </TableRow>
          ))}
        </TableBody>
        <TableFooter>
          <TableRow>
            <TableCell colSpan={4}>Total</TableCell>
            <TableCell className="text-right">$2,250.00</TableCell>
          </TableRow>
        </TableFooter>
      </Table>
    </Card>
  )
}

Usage

import {
  Card,
  Table,
  TableBody,
  TableCell,
  TableFooter,
  TableHead,
  TableHeader,
  TableRow,
} from 'ui'
<Card>
  <Table>
    <TableHeader>
      <TableRow>
        <TableHead>Column 1</TableHead>
        <TableHead>Column 2</TableHead>
        <TableHead>Column 3</TableHead>
      </TableRow>
    </TableHeader>
    <TableBody>
      <TableRow>
        <TableCell>Value 1</TableCell>
        <TableCell>Value 2</TableCell>
        <TableCell>Value 3</TableCell>
      </TableRow>
    </TableBody>
    <TableFooter>
      <TableRow>
        <TableCell colSpan={2}>Total</TableCell>
        <TableCell>Value</TableCell>
      </TableRow>
    </TableFooter>
  </Table>
</Card>

Layout

Columns will naturally take the width of their content unless otherwise specified. Specific column widths may be useful for controlling layout shift or for aligning multiple Table instances vertically.

Table Head includes a whitespace-nowrap utility class to prevent its text from wrapping. Table Cell must have any custom width specified, as there are legitimate use cases for both wrapping (or truncated) text.

The Table component is wrapped in a ShadowScrollArea by default, which provides horizontal scrolling on smaller screens when the table content exceeds the viewport width.

Structure

Table is comprised of the following Shadcn primitives which map directly to their HTML counterparts:

ComponentHTML element
Table<table>
Table Body<tbody>
Table Caption<caption>
Table Cell<td>
Table Footer<tfoot>
Table Head<th>
Table Header<thead>
Table Row<tr>

Table and its child components follow their HTML element counterparts for semantics, accessibility, and best practices.

Table Caption

Table Caption describes a table’s purpose or content. Think of it like the label of a diagram. It should only contain a single text node.

Just like its <caption> counterpart, Table Caption should be the first child of a Table element (before a Table Body) irrespective of its final visual positioning.

Table Footer is a semantic grouping for the final rows of a table, typically used for column summaries, totals, or footnotes that relate to the data in the body of the table.

Table Footer typically contains the same child elements as a Table Body, such as Table Row and Table Cell, and is a sibling of Table Body.

Examples

Empty state

When displaying an empty state within a table, dim the Table Head text to indicate that no data is present. Use consistent zero results presentation across your application to maintain a cohesive user experience.

Loading...

To prevent the empty state row from being highlighted on hover, apply the [&>td]:hover:bg-inherit class to the Table Row containing the empty state message.

<TableRow className="[&>td]:hover:bg-inherit">
  <TableCell colSpan={3}>
    <p className="text-sm text-foreground">No results found</p>
    <p className="text-sm text-foreground-lighter">
      Your search for “test” did not return any results
    </p>
  </TableCell>
</TableRow>

See Empty States for more information.

Sortable columns

Use TableHeadSort inside Table Head to enable column sorting. This component provides visual indicators for sort state (ascending, descending, or unsorted) and handles click interactions.

import { TableHeadSort } from 'ui'
PropTypeDescription
columnstringUnique identifier for the column
currentSortstringCurrent sort state in format "column:order" (e.g., "name:asc")
onSortChange(column: string) => voidCallback fired when column header is clicked
childrenReactNodeThe label text for the column header
classNamestringOptional additional CSS classes
<TableHead>
  <TableHeadSort column="name" currentSort={sort} onSortChange={handleSortChange}>
    Name
  </TableHeadSort>
</TableHead>

The component displays:

  • An up arrow when the column is sorted ascending
  • A down arrow when the column is sorted descending
  • A chevrons icon when the column is not currently sorted (visible on hover)
Loading...

Row icons

When adding icon columns to your table, use Accessibility markup by including a screen reader-only label in the corresponding Table Head using the sr-only class. This ensures that assistive technologies can properly identify the column's purpose. Remove these icon cells when loading or displaying zero results to maintain a clean and consistent table structure.

Loading...
<Table>
  <TableHeader>
    <TableRow>
      <TableHead className="w-1">
        <span className="sr-only">Icon</span>
      </TableHead>
      <TableHead>Name</TableHead>
      <TableHead>Email</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    <TableRow>
      <TableCell className="w-1">
        <IconName size={16} className="text-foreground-muted" />
      </TableCell>
      <TableCell>Value</TableCell>
      <TableCell>Value</TableCell>
    </TableRow>
  </TableBody>
</Table>

Actions

Action should be placed in the last column of each Table Row to maintain a logical reading flow. Users scan table data from left to right, so positioning actions on the right ensures they encounter the primary information first before reaching interactive controls.

Multiple actions

When multiple actions are available for a row, display one primary action as a button and place additional options in an overflow menu. This keeps the table clean and scannable while providing access to all necessary actions without overwhelming the user.

Loading...

You may need to link to related resources from within table cells. In these cases, use the text-link-table-cell CSS class to provide a visual hint that the text is interactive while maintaining its appearance as tertiary content.

Loading...

Avoid making the entire row interactive when using cross-links. Multiple interactive areas within a single row increases the chance of mis-taps and creates ambiguity about which action will be triggered. Keep cross-links as discrete, scannable elements within their respective cells, and maintain a dedicated action column for primary row-level actions.

Row-level navigation

The entire Table Row may be tappable. This pattern is most effective when navigation is the sole or primary action for each row, as it provides a large, easy-to-target interaction area.

Avoid adding other actions when using row-level navigation, as multiple interactive areas within a single row create competing affordances and increase interaction complexity.

Loading...

When implementing row-level navigation, pay close attention to Accessibility requirements. The row must be keyboard accessible with proper focus management, including:

  • Handling Enter and Space key presses for activation
  • Providing visual focus indicators using classes like inset-focus
  • Supporting modifier keys (Ctrl/Cmd) for opening links in new tabs

Row navigation with actions

If you must combine row-level navigation with additional actions, nest all secondary actions in an overflow menu and maintain a ChevronRight visual indicator to signal that the row is navigable. This pattern preserves the primary navigation affordance while keeping secondary actions accessible but unobtrusive.

Loading...

This hybrid approach requires careful attention to event handling. Ensure that taps on action buttons stop event propagation to prevent triggering row navigation, and maintain clear visual separation between the navigable row area and the action controls.

Use the Table component for simple, static tabular data presentation with a fixed number of rows. Consider using Data Table for more complex cases as it provides built-in sorting, filtering, and pagination capabilities.

Refer to Tables for broader guidance and best practices on presenting tabular data.