apps/docs/content/docs/react/migration/(components)/table.mdx
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>
);
}
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
Table is now the root container (styling wrapper)Table.ScrollContainer handles horizontal scrolling with custom scrollbarTable.Content is the actual <table> element (where aria-label, selectionMode, sortDescriptor etc. go)Table.Footer replaces bottomContent for pagination and other footer content| 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 |
v2: Checkboxes auto-rendered by the table when selectionMode is set
v3: Use Checkbox with slot="selection" explicitly in columns and rows
v2: loadingState, loadingContent, emptyContent props on TableBody
v3: renderEmptyState prop on Table.Body; use Table.LoadMore for infinite scroll loading
v2: bottomContent prop on Table
v3: Table.Footer compound component
v2: Not built-in
v3: Table.ResizableContainer + Table.ColumnResizer compound components
<Tabs items={["v2", "v3"]}> <Tab value="v2"> ```tsx 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>
```
<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>
```
<Tabs items={["v2", "v3"]}> <Tab value="v2"> ```tsx 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>
```
<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>
```
<Tabs items={["v2", "v3"]}> <Tab value="v2"> ```tsx 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>
```
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>
```
<Tabs items={["v2", "v3"]}> <Tab value="v2"> ```tsx 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>
```
<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>
```
<Tabs items={["v2", "v3"]}>
<Tab value="v2">
tsx <Table aria-label="Users" bottomContent={ <Pagination total={10} page={page} onChange={setPage} /> } bottomContentPlacement="outside" > </Table>
</Tab>
<Tab value="v3">
tsx <Table> <Table.ScrollContainer> <Table.Content aria-label="Users"> </Table.Content> </Table.ScrollContainer> <Table.Footer> </Table.Footer> </Table>
</Tab>
</Tabs>
<Tabs items={["v2", "v3"]}>
<Tab value="v2">
tsx <TableBody emptyContent="No rows to display."> {[]} </TableBody>
</Tab>
<Tab value="v3">
tsx <Table.Body items={[]} renderEmptyState={() => ( <p className="text-center py-4">No rows to display.</p> )} > {[]} </Table.Body>
</Tab>
</Tabs>
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",
}}
/>
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>
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.)
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.
Table import with dot notationTable.ScrollContainer and Table.Content wrap the table structurearia-label, selectionMode, sortDescriptor, etc. moved from Table to Table.ContentbottomContent prop → Table.Footer compound componenttopContent prop → place content inside Table before Table.ScrollContainerCheckbox with slot="selection"emptyContent prop → renderEmptyState on Table.BodyloadingState/loadingContent → Table.LoadMore for infinite scrollTable.ResizableContainer and Table.ColumnResizerkey → id on rows and columnscolor, radius, shadow, isStriped, isCompact → use Tailwind CSSclassName on individual compound components