Table
Table is a composite component: you assemble the structure manually
using subcomponents (TableHeader, TableBody, TableRow, TableCell,
etc.). Unlike DataTable, there is no
virtualization here — the component renders a semantic HTML <table>, ideal
for small/medium tables where accessibility and structure matter.
Includes optional search, a pagination bar (display-only or navigable — you own the page state), column totals, selection checkboxes, density control and sticky header support.
When to use
| ✅ Use when… | 🚫 Avoid when… |
|---|---|
|
|
Table elements
The composite API uses the following components:
Table— root, controls state (search, selection, density).TableHeader(Thead) — header (<thead>).TableBody(Tbody) — body (<tbody>).TableRow(Tr) — row (<tr>).TableCell(Td) — data cell (<td>).TableHeaderCell(Th) — header cell (<th>).TableCheckboxCell(Tdc) — cell with a selection checkbox.TableHeaderCheckboxCell(Thc) — header with a "select all" checkbox.TableFooter(Tfooter) — footer (<tfoot>).TableEmpty— empty state (rendered automatically when the search returns no results).TablePagination— pagination bar (range, rows per page, optional previous/next navigation).
The short aliases (Thead, Tbody, Tr, etc.) and the long notation
(TableHeader, TableBody, etc.) point to the same component — use
whichever matches your project's style.
Basic usage
Mini Pokedex
Pokedex No. | Name | Type |
|---|---|---|
001 | Bulbasaur | Grass |
004 | Charmander | Fire |
007 | Squirtle | Water |
025 | Pikachu | Electric |
import {
Table,
TableHeader,
TableBody,
TableRow,
TableCell,
TableHeaderCell,
} from '@apollion-dsi/core/data-display/table';
<Table title="Mini Pokedex">
<TableHeader>
<TableRow>
<TableHeaderCell>Pokedex No.</TableHeaderCell>
<TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>Type</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>001</TableCell>
<TableCell>Bulbasaur</TableCell>
<TableCell>Grass</TableCell>
</TableRow>
</TableBody>
</Table>;Search
Enable with searching. The input appears above the table; when typing and
pressing Enter, rows that do not match the term are hidden via
display: none.
Pokedex with search
No. | Name | Type |
|---|---|---|
001 | Bulbasaur | Grass |
004 | Charmander | Fire |
007 | Squirtle | Water |
025 | Pikachu | Electric |
<Table title="Mini Pokedex" searching>
{/* ... */}
</Table>Striped and densities
striped applies zebra striping; rowSize controls density (Compact,
Default, Expansive). With rowTypes, the user switches density at
runtime via a dropdown.
Striped + compact
No. | Name | Type |
|---|---|---|
001 | Bulbasaur | Grass |
004 | Charmander | Fire |
007 | Squirtle | Water |
025 | Pikachu | Electric |
<Table striped rowSize="Compact">{/* ... */}</Table>
<Table rowTypes>{/* dropdown enabled */}</Table>Sticky header
stickyHeader pins the <thead> to the top while scrolling — combine with
a wrapper that has max-height so the scroll actually happens:
<Paper maxHeight="500px">
<Table stickyHeader>{/* ... */}</Table>
</Paper>Alignment
Global (string) or per column (array). Pick one mode per Table
instance — mixing a global string with a per-column array on the same
table is not supported:
<Table align="right">{/* ... */}</Table>
<Table
align={[
{ direction: 'left', target: '3' },
{ direction: 'right', target: '5' },
]}
>{/* ... */}</Table>Per-column width
columnSize takes an array — each entry becomes width (without type),
min-width or max-width depending on type. Empty strings keep the
natural width:
<Table columnSize={[{ width: '100' }, '', { width: 250, type: 'min' }]}>{/* ... */}</Table>Ellipsis
Truncates long content with … and shows the full text in a tooltip on
hover. Useful for narrow tables with variable content.
Truncated descriptions
ID | Description | Status |
|---|---|---|
001 | Implement the CSV export feature with advanced filters. | OK |
002 | Refactor the authentication module to support SSO via SAML 2.0. | In progress |
003 | Update backend dependencies to LTS versions. | OK |
<Table ellipsis>{/* ... */}</Table>Hoverable
Highlights the row under the cursor — useful in long tables for visual feedback on which row is being read.
Hoverable rows
No. | Name | Type |
|---|---|---|
001 | Bulbasaur | Grass |
004 | Charmander | Fire |
007 | Squirtle | Water |
025 | Pikachu | Electric |
<Table hoverable>{/* ... */}</Table>Checkbox selection
Use TableCheckboxCell in the first cell of each TableBody row,
and optionally TableHeaderCheckboxCell for the "select all".
Multiple selection
Name | Type | |
|---|---|---|
Bulbasaur | Grass | |
Charmander | Fire | |
Squirtle | Water |
<Table>
<TableHeader>
<TableRow>
<TableHeaderCheckboxCell>Select</TableHeaderCheckboxCell>
<TableHeaderCell>Name</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCheckboxCell>001</TableCheckboxCell>
<TableCell>Bulbasaur</TableCell>
</TableRow>
</TableBody>
</Table>Pagination
The pagination bar is stateless: Table renders it, you own the page
index and the data slice. Provide the four range props together
(initialPagination, finalPagination, totalPagination, linesPerPage)
to display the bar; wire the callbacks to make it navigable.
Navigable
onPreviousPage/onNextPage render the previous/next buttons;
linesPerPageValue + onLinesPerPageChange control the rows-per-page
selector. The buttons' enabled state is derived from a numeric range
(previous while initialPagination > 1, next while
finalPagination < totalPagination) — pass canPreviousPage/canNextPage
to override it (formatted strings such as "1.234" keep both enabled).
Navigable pagination
No. | Name | Type |
|---|---|---|
001 | Bulbasaur | Grass |
004 | Charmander | Fire |
007 | Squirtle | Water |
const [page, setPage] = React.useState(0);
const [pageSize, setPageSize] = React.useState(10);
const start = page * pageSize;
const rows = data.slice(start, start + pageSize); // or the server slice
<Table
initialPagination={start + 1}
finalPagination={Math.min(start + pageSize, data.length)}
totalPagination={data.length}
linesPerPage={[
{ label: '10', value: 10 },
{ label: '20', value: 20 },
{ label: '30', value: 30 },
]}
linesPerPageValue={pageSize}
onLinesPerPageChange={(size) => {
setPageSize(size);
setPage(0);
}}
onPreviousPage={() => setPage((current) => current - 1)}
onNextPage={() => setPage((current) => current + 1)}
>
{/* rows */}
</Table>;Display-only
Without the callbacks the bar only shows the range and the selector (the historical behaviour):
Display-only pagination
No. | Name | Type |
|---|---|---|
001 | Bulbasaur | Grass |
004 | Charmander | Fire |
007 | Squirtle | Water |
<Table
initialPagination="1"
finalPagination="10"
totalPagination="30"
linesPerPage={[
{ label: '10', value: 10 },
{ label: '20', value: 20 },
{ label: '30', value: 30 },
]}
>
{/* ... */}
</Table>Labels: showingText, linesPerPageLabel, previousLabel, nextLabel
(pt-BR defaults) — see TablePagination.
Column totals
columnSum is the (0-based) index of the column to sum. Render an
empty <TableFooter /> — it will be filled automatically with
"Total: X":
Column totals
SKU | Product | Qty |
|---|---|---|
A-01 | Shirt | 3 |
A-02 | Sneakers | 1 |
A-03 | Cap | 5 |
Total: |
<Table columnSum={4}>
{/* ...header and body... */}
<TableFooter />
</Table>Sharing
Enable the share button (email/social network) with share:
<Table
share={{
email: () => sendByEmail(),
social: () => shareOnSocial(),
}}
>
{/* ... */}
</Table>Empty state
TableEmpty is rendered automatically when all rows are
hidden by the search. For custom placeholders, assemble it manually:
No records
Try adjusting the filters.
import { TableEmpty } from '@apollion-dsi/core/data-display/table';
<TableEmpty titleEmpty="No records" subtitleEmpty="Try adjusting the filters." />;See also
- Storybook story: Components / Table
- Full API:
TableInterface(reference generated from TSDoc). - High-performance virtualized table:
DataTable.