Pro--% off in--d : --h : --m : --s
HeroUI

Table

Migration guide for Table from HeroUI v2 to v3

Refer to the v3 Table documentation for complete API reference, styling guide, and advanced examples. This guide only focuses on migrating from HeroUI v2.

Structure Changes

In v2, Table used separate named imports for each part:

import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell } from "@heroui/react";

export default function App() {
  return (
    <Table aria-label="Example table">
      <TableHeader>
        <TableColumn>Name</TableColumn>
        <TableColumn>Role</TableColumn>
      </TableHeader>
      <TableBody>
        <TableRow key="1">
          <TableCell>Kate Moore</TableCell>
          <TableCell>CEO</TableCell>
        </TableRow>
      </TableBody>
    </Table>
  );
}

In v3, Table uses the compound component pattern with dot notation and adds Table.ScrollContainer and Table.Content:

import { Table } from "@heroui/react";

export default function App() {
  return (
    <Table>
      <Table.ScrollContainer>
        <Table.Content aria-label="Example table">
          <Table.Header>
            <Table.Column>Name</Table.Column>
            <Table.Column>Role</Table.Column>
          </Table.Header>
          <Table.Body>
            <Table.Row>
              <Table.Cell>Kate Moore</Table.Cell>
              <Table.Cell>CEO</Table.Cell>
            </Table.Row>
          </Table.Body>
        </Table.Content>
      </Table.ScrollContainer>
    </Table>
  );
}

Key Changes

1. Component Structure

v2: Separate named imports (Table, TableHeader, TableColumn, TableBody, TableRow, TableCell) v3: Single import with dot notation: Table, Table.ScrollContainer, Table.Content, Table.Header, Table.Column, Table.Body, Table.Row, Table.Cell, Table.Footer

2. New Wrapper Components

  • Table is now the root container (styling wrapper)
  • Table.ScrollContainer handles horizontal scrolling with custom scrollbar
  • Table.Content is the actual <table> element (where aria-label, selectionMode, sortDescriptor etc. go)
  • Table.Footer replaces bottomContent for pagination and other footer content

3. Prop Changes

v2 Propv3 EquivalentNotes
aria-labelTable.Content aria-labelMoved to Table.Content
selectionModeTable.Content selectionModeMoved to Table.Content
selectedKeysTable.Content selectedKeysMoved to Table.Content
defaultSelectedKeysTable.Content defaultSelectedKeysMoved to Table.Content
onSelectionChangeTable.Content onSelectionChangeMoved to Table.Content
sortDescriptorTable.Content sortDescriptorMoved to Table.Content
onSortChangeTable.Content onSortChangeMoved to Table.Content
disabledKeysTable.Content disabledKeysMoved to Table.Content
disallowEmptySelectionTable.Content disallowEmptySelectionMoved to Table.Content
selectionBehaviorTable.Content selectionBehaviorMoved to Table.Content
disabledBehaviorTable.Content disabledBehaviorMoved to Table.Content
onRowActionTable.Content onRowActionMoved to Table.Content
onCellActionTable.Content onCellActionMoved to Table.Content
topContent-Place content inside Table before Table.ScrollContainer
bottomContent-Use Table.Footer
topContentPlacement-Removed (compose layout directly)
bottomContentPlacement-Removed (compose layout directly)
color-Removed (use Tailwind CSS)
variantTable variantChanged to "primary" (card-style, default) or "secondary" (flat)
layout-Removed
radius-Removed (use Tailwind CSS)
shadow-Removed (use Tailwind CSS)
isStriped-Removed (use Tailwind CSS)
isCompact-Removed (use Tailwind CSS)
isHeaderSticky-Removed (use Tailwind CSS sticky top-0)
fullWidth-Removed (full width by default)
removeWrapper-Removed (compose layout directly)
hideHeader-Removed (omit Table.Header or use CSS)
isVirtualized-Use React Aria Virtualizer wrapper
maxTableHeight-Use CSS or Virtualizer
rowHeight-Use TableLayout with Virtualizer
isKeyboardNavigationDisabled-Removed
disableAnimation-Removed
classNames-Use className on individual compound components

4. Selection with Checkboxes

v2: Checkboxes auto-rendered by the table when selectionMode is set v3: Use Checkbox with slot="selection" explicitly in columns and rows

5. Loading and Empty States

v2: loadingState, loadingContent, emptyContent props on TableBody v3: renderEmptyState prop on Table.Body; use Table.LoadMore for infinite scroll loading

6. Pagination

v2: bottomContent prop on Table v3: Table.Footer compound component

7. Column Resizing

v2: Not built-in v3: Table.ResizableContainer + Table.ColumnResizer compound components

Migration Examples

Basic Table

import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell } from "@heroui/react";

<Table aria-label="Users">
  <TableHeader>
    <TableColumn>Name</TableColumn>
    <TableColumn>Role</TableColumn>
    <TableColumn>Status</TableColumn>
  </TableHeader>
  <TableBody>
    <TableRow key="1">
      <TableCell>Kate Moore</TableCell>
      <TableCell>CEO</TableCell>
      <TableCell>Active</TableCell>
    </TableRow>
    <TableRow key="2">
      <TableCell>John Doe</TableCell>
      <TableCell>Developer</TableCell>
      <TableCell>Active</TableCell>
    </TableRow>
  </TableBody>
</Table>
import { Table } from "@heroui/react";

<Table>
  <Table.ScrollContainer>
    <Table.Content aria-label="Users">
      <Table.Header>
        <Table.Column>Name</Table.Column>
        <Table.Column>Role</Table.Column>
        <Table.Column>Status</Table.Column>
      </Table.Header>
      <Table.Body>
        <Table.Row id="1">
          <Table.Cell>Kate Moore</Table.Cell>
          <Table.Cell>CEO</Table.Cell>
          <Table.Cell>Active</Table.Cell>
        </Table.Row>
        <Table.Row id="2">
          <Table.Cell>John Doe</Table.Cell>
          <Table.Cell>Developer</Table.Cell>
          <Table.Cell>Active</Table.Cell>
        </Table.Row>
      </Table.Body>
    </Table.Content>
  </Table.ScrollContainer>
</Table>

Dynamic Rows

const columns = [
  { key: "name", label: "Name" },
  { key: "role", label: "Role" },
];
const rows = [
  { key: "1", name: "Kate", role: "CEO" },
  { key: "2", name: "John", role: "Developer" },
];

<Table aria-label="Users">
  <TableHeader columns={columns}>
    {(column) => <TableColumn key={column.key}>{column.label}</TableColumn>}
  </TableHeader>
  <TableBody items={rows}>
    {(item) => (
      <TableRow key={item.key}>
        {(columnKey) => <TableCell>{item[columnKey]}</TableCell>}
      </TableRow>
    )}
  </TableBody>
</Table>
const columns = [
  { id: "name", label: "Name" },
  { id: "role", label: "Role" },
];
const rows = [
  { id: "1", name: "Kate", role: "CEO" },
  { id: "2", name: "John", role: "Developer" },
];

<Table>
  <Table.ScrollContainer>
    <Table.Content aria-label="Users">
      <Table.Header columns={columns}>
        {(column) => <Table.Column id={column.id}>{column.label}</Table.Column>}
      </Table.Header>
      <Table.Body items={rows}>
        {(item) => (
          <Table.Row id={item.id}>
            <Table.Cell>{item.name}</Table.Cell>
            <Table.Cell>{item.role}</Table.Cell>
          </Table.Row>
        )}
      </Table.Body>
    </Table.Content>
  </Table.ScrollContainer>
</Table>

Selection

const [selectedKeys, setSelectedKeys] = useState(new Set(["1"]));

<Table
  aria-label="Users"
  selectionMode="multiple"
  selectedKeys={selectedKeys}
  onSelectionChange={setSelectedKeys}
>
  <TableHeader>
    <TableColumn>Name</TableColumn>
    <TableColumn>Role</TableColumn>
  </TableHeader>
  <TableBody>
    <TableRow key="1">
      <TableCell>Kate</TableCell>
      <TableCell>CEO</TableCell>
    </TableRow>
    <TableRow key="2">
      <TableCell>John</TableCell>
      <TableCell>Developer</TableCell>
    </TableRow>
  </TableBody>
</Table>
import { Table, Checkbox } from "@heroui/react";

const [selectedKeys, setSelectedKeys] = useState(new Set(["1"]));

<Table>
  <Table.ScrollContainer>
    <Table.Content
      aria-label="Users"
      selectionMode="multiple"
      selectedKeys={selectedKeys}
      onSelectionChange={setSelectedKeys}
    >
      <Table.Header>
        <Table.Column>
          <Checkbox slot="selection" />
        </Table.Column>
        <Table.Column>Name</Table.Column>
        <Table.Column>Role</Table.Column>
      </Table.Header>
      <Table.Body>
        <Table.Row id="1">
          <Table.Cell>
            <Checkbox slot="selection" />
          </Table.Cell>
          <Table.Cell>Kate</Table.Cell>
          <Table.Cell>CEO</Table.Cell>
        </Table.Row>
        <Table.Row id="2">
          <Table.Cell>
            <Checkbox slot="selection" />
          </Table.Cell>
          <Table.Cell>John</Table.Cell>
          <Table.Cell>Developer</Table.Cell>
        </Table.Row>
      </Table.Body>
    </Table.Content>
  </Table.ScrollContainer>
</Table>

Sorting

const [sortDescriptor, setSortDescriptor] = useState({
  column: "name",
  direction: "ascending",
});

<Table
  aria-label="Users"
  sortDescriptor={sortDescriptor}
  onSortChange={setSortDescriptor}
>
  <TableHeader>
    <TableColumn key="name" allowsSorting>Name</TableColumn>
    <TableColumn key="role" allowsSorting>Role</TableColumn>
  </TableHeader>
  <TableBody items={sortedItems}>
    {(item) => (
      <TableRow key={item.key}>
        {(columnKey) => <TableCell>{item[columnKey]}</TableCell>}
      </TableRow>
    )}
  </TableBody>
</Table>
const [sortDescriptor, setSortDescriptor] = useState({
  column: "name",
  direction: "ascending",
});

<Table>
  <Table.ScrollContainer>
    <Table.Content
      aria-label="Users"
      sortDescriptor={sortDescriptor}
      onSortChange={setSortDescriptor}
    >
      <Table.Header>
        <Table.Column id="name" allowsSorting>Name</Table.Column>
        <Table.Column id="role" allowsSorting>Role</Table.Column>
      </Table.Header>
      <Table.Body items={sortedItems}>
        {(item) => (
          <Table.Row id={item.id}>
            <Table.Cell>{item.name}</Table.Cell>
            <Table.Cell>{item.role}</Table.Cell>
          </Table.Row>
        )}
      </Table.Body>
    </Table.Content>
  </Table.ScrollContainer>
</Table>

With Pagination (Bottom Content)

<Table
  aria-label="Users"
  bottomContent={
    <Pagination total={10} page={page} onChange={setPage} />
  }
  bottomContentPlacement="outside"
>
  {/* ... */}
</Table>
<Table>
  <Table.ScrollContainer>
    <Table.Content aria-label="Users">
      {/* Header and Body */}
    </Table.Content>
  </Table.ScrollContainer>
  <Table.Footer>
    {/* Pagination component */}
  </Table.Footer>
</Table>

Empty State

<TableBody emptyContent="No rows to display.">
  {[]}
</TableBody>
<Table.Body
  items={[]}
  renderEmptyState={() => (
    <p className="text-center py-4">No rows to display.</p>
  )}
>
  {[]}
</Table.Body>

Styling Changes

v2: classNames Prop

<Table
  classNames={{
    base: "custom-base",
    wrapper: "custom-wrapper",
    table: "custom-table",
    thead: "custom-header",
    tbody: "custom-body",
    tr: "custom-row",
    th: "custom-column",
    td: "custom-cell",
  }}
/>

v3: Direct className Props

<Table className="custom-base">
  <Table.ScrollContainer className="custom-wrapper">
    <Table.Content aria-label="Table" className="custom-table">
      <Table.Header className="custom-header">
        <Table.Column className="custom-column">Name</Table.Column>
      </Table.Header>
      <Table.Body className="custom-body">
        <Table.Row className="custom-row">
          <Table.Cell className="custom-cell">Value</Table.Cell>
        </Table.Row>
      </Table.Body>
    </Table.Content>
  </Table.ScrollContainer>
</Table>

Component Anatomy

The v3 Table follows this structure:

Table (Root container)
  ├── Table.ScrollContainer (horizontal scroll)
  │   └── Table.Content (<table> element, aria-label, selectionMode, etc.)
  │       ├── Table.Header (<thead>)
  │       │   └── Table.Column (<th>, allowsSorting, etc.)
  │       │       └── Table.ColumnResizer (optional)
  │       └── Table.Body (<tbody>, items, renderEmptyState)
  │           ├── Table.Row (<tr>)
  │           │   └── Table.Cell (<td>)
  │           └── Table.LoadMore (optional, infinite scroll)
  │               └── Table.LoadMoreContent
  └── Table.Footer (optional, pagination, etc.)

Item Identity

v2: React's key was used for both list reconciliation and selection state. v3: Use id on Table.Row and Table.Column for selection/sort state; keep React's key for lists.

Summary

  1. Imports: Separate named imports → single Table import with dot notation
  2. New Wrappers: Table.ScrollContainer and Table.Content wrap the table structure
  3. Props Moved: aria-label, selectionMode, sortDescriptor, etc. moved from Table to Table.Content
  4. Bottom Content: bottomContent prop → Table.Footer compound component
  5. Top Content: topContent prop → place content inside Table before Table.ScrollContainer
  6. Selection Checkboxes: Auto-rendered → explicit Checkbox with slot="selection"
  7. Empty State: emptyContent prop → renderEmptyState on Table.Body
  8. Loading: loadingState/loadingContentTable.LoadMore for infinite scroll
  9. Column Resizing: New Table.ResizableContainer and Table.ColumnResizer
  10. Item Identity: keyid on rows and columns
  11. Styling Props Removed: color, radius, shadow, isStriped, isCompact → use Tailwind CSS
  12. ClassNames Removed: Use className on individual compound components

On this page