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
Tableis now the root container (styling wrapper)Table.ScrollContainerhandles horizontal scrolling with custom scrollbarTable.Contentis the actual<table>element (wherearia-label,selectionMode,sortDescriptoretc. go)Table.FooterreplacesbottomContentfor pagination and other footer content
3. Prop Changes
| v2 Prop | v3 Equivalent | Notes |
|---|---|---|
aria-label | Table.Content aria-label | Moved to Table.Content |
selectionMode | Table.Content selectionMode | Moved to Table.Content |
selectedKeys | Table.Content selectedKeys | Moved to Table.Content |
defaultSelectedKeys | Table.Content defaultSelectedKeys | Moved to Table.Content |
onSelectionChange | Table.Content onSelectionChange | Moved to Table.Content |
sortDescriptor | Table.Content sortDescriptor | Moved to Table.Content |
onSortChange | Table.Content onSortChange | Moved to Table.Content |
disabledKeys | Table.Content disabledKeys | Moved to Table.Content |
disallowEmptySelection | Table.Content disallowEmptySelection | Moved to Table.Content |
selectionBehavior | Table.Content selectionBehavior | Moved to Table.Content |
disabledBehavior | Table.Content disabledBehavior | Moved to Table.Content |
onRowAction | Table.Content onRowAction | Moved to Table.Content |
onCellAction | Table.Content onCellAction | Moved 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) |
variant | Table variant | Changed 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
- Imports: Separate named imports → single
Tableimport with dot notation - New Wrappers:
Table.ScrollContainerandTable.Contentwrap the table structure - Props Moved:
aria-label,selectionMode,sortDescriptor, etc. moved fromTabletoTable.Content - Bottom Content:
bottomContentprop →Table.Footercompound component - Top Content:
topContentprop → place content insideTablebeforeTable.ScrollContainer - Selection Checkboxes: Auto-rendered → explicit
Checkboxwithslot="selection" - Empty State:
emptyContentprop →renderEmptyStateonTable.Body - Loading:
loadingState/loadingContent→Table.LoadMorefor infinite scroll - Column Resizing: New
Table.ResizableContainerandTable.ColumnResizer - Item Identity:
key→idon rows and columns - Styling Props Removed:
color,radius,shadow,isStriped,isCompact→ use Tailwind CSS - ClassNames Removed: Use
classNameon individual compound components