Tables
Tables can be used to present users with interactive tabular information. A table can be a simple listing with 3-4 columns, but it can also be a highly interactive widget that allows users to select rows, hide and/or sort columns, and perform custom row actions. Because of this complexity, Cimpress UI doesn’t provide a single all-encompassing table component. Instead, it provides building blocks that allow you to build your own table that’s tailored to the needs of your application and your dataset.
This guide will show you how to build your own table from Cimpress UI building blocks, and will walk you through any additional considerations for making your table accessible to more people.
Data source
Section titled “Data source”Cimpress UI doesn’t assume anything about your data source. Our components help you display the data and all necessary controls, but they don’t contain any data logic. It’s up to you to modify your data based on actions performed by users.
While you can wire up a simple table by yourself using React state, we recommend using a headless data engine for more complex scenarios. Such an engine handles all state and interactions for you.
We recommend using TanStack Table as your data engine. We also provide the @cimpress-ui/tanstack-table-react
package, which contains a set of components and hooks that seamlessly connect TanStack Table with Cimpress UI components.
To use this package, install it as a dependency in your application alongside Cimpress UI:
npm install @cimpress-ui/tanstack-table-react
pnpm add @cimpress-ui/tanstack-table-react
yarn add @cimpress-ui/tanstack-table-react
You do not need to install TanStack Table separately. All exports from @tanstack/react-table
are re-exported from @cimpress-ui/tanstack-table-react/base
.
Prerequisites
Section titled “Prerequisites”We are going to build a table showcasing the highest-rated movies. This is what our data looks like:
export interface Movie { title: string; releaseYear: number; rating: number;}
export const movies: Movie[] = [ { title: 'The Shawshank Redemption', releaseYear: 1994, rating: 9.3 }, { title: 'The Godfather', releaseYear: 1972, rating: 9.2 }, { title: 'The Godfather: Part II', releaseYear: 1974, rating: 9.0 }, { title: 'The Dark Knight', releaseYear: 2008, rating: 9.0 }, { title: '12 Angry Men', releaseYear: 1957, rating: 8.9 }, { title: "Schindler's List", releaseYear: 1993, rating: 8.9 }, { title: 'The Lord of the Rings: The Return of the King', releaseYear: 2003, rating: 8.9 }, { title: 'Pulp Fiction', releaseYear: 1994, rating: 8.9 }, { title: 'The Lord of the Rings: The Fellowship of the Ring', releaseYear: 2001, rating: 8.8 }, { title: 'Forrest Gump', releaseYear: 1994, rating: 8.8 },];
Additionally, we’ll need a React component to render our table:
export function MovieTable() { return 'This will be a table';}
Basic data table
Section titled “Basic data table”First, let’s build a simple table with no interactivity. Choose the “Manual” or “TanStack” tab, depending on how you intend to build your table.
-
Build a basic table structure using Cimpress UI components. Don’t forget to label your table!
table.tsx import { Table, TableBody, TableContainer, TableHeader } from '@cimpress-ui/react';export function MovieTable() {return 'This will be a table';return (<TableContainer><Table aria-label="Highest-rated movies"><TableHeader /><TableBody /></Table></TableContainer>);} -
Add a header row based on your data structure. Make sure that each header cell has a unique
columnKey
.table.tsx import { Table, TableBody, TableContainer, TableHeader } from '@cimpress-ui/react';import { Table, TableBody, TableContainer, TableHeader, TableHeaderCell, TableHeaderRow } from '@cimpress-ui/react';export function MovieTable() {return (<TableContainer><Table aria-label="Highest-rated movies"><TableHeader /><TableHeader><TableHeaderRow><TableHeaderCell columnKey="title">Title</TableHeaderCell><TableHeaderCell columnKey="release-year">Release year</TableHeaderCell><TableHeaderCell columnKey="rating">Rating</TableHeaderCell></TableHeaderRow></TableHeader><TableBody /></Table></TableContainer>);} -
Map over your data to create table rows. Make sure that each body cell has a
columnKey
that’s the same as thecolumnKey
of the corresponding header cell.table.tsx import {Table,TableBody,TableBodyCell,TableBodyRow,TableContainer,TableHeader,TableHeaderCell,TableHeaderRow,} from '@cimpress-ui/react';import { movies } from './data.js';export function MovieTable() {return (<TableContainer><Table aria-label="Highest-rated movies"><TableHeader><TableHeaderRow><TableHeaderCell columnKey="title">Title</TableHeaderCell><TableHeaderCell columnKey="release-year">Release year</TableHeaderCell><TableHeaderCell columnKey="rating">Rating</TableHeaderCell></TableHeaderRow></TableHeader><TableBody /><TableBody>{movies.map((movie) => (<TableBodyRow key={movie.title}><TableBodyCell columnKey="title">{movie.title}</TableBodyCell><TableBodyCell columnKey="release-year">{movie.releaseYear}</TableBodyCell><TableBodyCell columnKey="rating">{movie.rating.toFixed(1)}</TableBodyCell></TableBodyRow>))}</TableBody></Table></TableContainer>);}
-
Define the columns. Consult the TanStack Table documentation for details.
columns.ts import { createColumnHelper } from '@cimpress-ui/tanstack-table-react/base';import type { Movie } from './data.js';const columnHelper = createColumnHelper<Movie>();export const columns = [columnHelper.accessor('title', {id: 'title',header: 'Title',}),columnHelper.accessor('releaseYear', {id: 'release-year',header: 'Release year',}),columnHelper.accessor('rating', {id: 'rating',header: 'Rating',cell: (ctx) => ctx.getValue().toFixed(1),}),]; -
Create a data engine using column definitions from step 1.
table.tsx import { useTableEngine } from '@cimpress-ui/tanstack-table-react';import { getCoreRowModel } from '@cimpress-ui/tanstack-table-react/base';import { columns } from './columns.js';import { type Movie, movies } from './data.js';export function MovieTable() {const engine = useTableEngine<Movie>({columns,data: movies,getCoreRowModel: getCoreRowModel(),});return 'This will be a table';} -
Render the table using the data engine created in step 2. Don’t forget to label your table!
table.tsx import { useTableEngine } from '@cimpress-ui/tanstack-table-react';import { Table, useTableEngine } from '@cimpress-ui/tanstack-table-react';import { getCoreRowModel } from '@cimpress-ui/tanstack-table-react/base';import { columns } from './columns.js';import { type Movie, movies } from './data.js';export function MovieTable() {const engine = useTableEngine<Movie>({columns,data: movies,getCoreRowModel: getCoreRowModel(),});return 'This will be a table';return <Table engine={engine} aria-label="Highest-rated movies" />;}
Column properties
Section titled “Column properties”Let’s make our table look a little better. We’ll assign specific widths to the columns, and align the content of “Release year” and “Rating” columns to the right.
// ...<TableHeader> <TableHeaderRow> <TableHeaderCell columnKey="title">Title</TableHeaderCell> <TableHeaderCell columnKey="title" columnWidth="60%"> Title </TableHeaderCell> <TableHeaderCell columnKey="release-year">Release year</TableHeaderCell> <TableHeaderCell columnKey="release-year" columnWidth="20%" columnContentAlignment="end"> Release year </TableHeaderCell> <TableHeaderCell columnKey="rating">Rating</TableHeaderCell> <TableHeaderCell columnKey="rating" columnWidth="20%" columnContentAlignment="end"> Rating </TableHeaderCell> </TableHeaderRow></TableHeader>// ...
// ...export const columns = [ columnHelper.accessor('title', { id: 'title', header: 'Title', width: '60%', }), columnHelper.accessor('releaseYear', { id: 'release-year', header: 'Release year', width: '20%', contentAlignment: 'end', }), columnHelper.accessor('rating', { id: 'rating', header: 'Rating', cell: (ctx) => ctx.getValue().toFixed(1), width: '20%', contentAlignment: 'end', }),];
Cell content
Section titled “Cell content”Make sure that the information within each cell can be conveyed by a screen reader. If a cell contains decorative elements that shouldn’t be read (icons, emojis, etc.), mark them with the aria-hidden
attribute.
Let’s add a star symbol to our rating column. As an example, if a cell contains the text “⭐️ 9.3”, a screen reader will read it as “star nine point three”. To make it read “nine point three”, mark the star and the space next to it with the aria-hidden
attribute.
// ...<TableBody> {movies.map((movie) => ( <TableBodyRow key={movie.title}> <TableBodyCell columnKey="title">{movie.title}</TableBodyCell> <TableBodyCell columnKey="release-year">{movie.releaseYear}</TableBodyCell> <TableBodyCell columnKey="rating">{movie.rating.toFixed(1)}</TableBodyCell> <TableBodyCell columnKey="rating"> <span aria-hidden>⭐️ </span> {movie.rating.toFixed(1)} </TableBodyCell> </TableBodyRow> ))}</TableBody>// ...
// ...export const columns = [ // ... columnHelper.accessor('rating', { id: 'rating', header: 'Rating', cell: (ctx) => ctx.getValue().toFixed(1), cell: (ctx) => ( <> <span aria-hidden>⭐️ </span> {ctx.getValue().toFixed(1)} </> ), width: '20%', contentAlignment: 'end', }),];
Final result
Section titled “Final result”This is what our table looks like now. All following chapters will use this table as a starting point.
Title | Release year | Rating |
---|---|---|
The Shawshank Redemption | 1994 | 9.3 |
The Godfather | 1972 | 9.2 |
The Godfather: Part II | 1974 | 9.0 |
The Dark Knight | 2008 | 9.0 |
12 Angry Men | 1957 | 8.9 |
Schindler's List | 1993 | 8.9 |
The Lord of the Rings: The Return of the King | 2003 | 8.9 |
Pulp Fiction | 1994 | 8.9 |
The Lord of the Rings: The Fellowship of the Ring | 2001 | 8.8 |
Forrest Gump | 1994 | 8.8 |
Working with partial data
Section titled “Working with partial data”There are situations when your table can present partial data, such as when using pagination, or when not all columns are visible on the page. While this is usually obvious for sighted users based on the presence of additional UI controls, screen reader users may find it hard to orient themselves within such a table. To alleviate this, you should provide some additional information to table components.
Subset of rows
Section titled “Subset of rows”If your table presents a subset of rows from a larger dataset (such as when using pagination), you should provide the following additional information:
- The number of rows in the full unpaginated dataset, including headers.
- The position of each row within the full unpaginated dataset, including headers.
Let’s paginate our table, displaying 3 rows per page:
-
Define variables required for pagination.
table.tsx // ...const pageSize = 3;const pageCount = Math.floor(movies.length / 3) + 1;export function MovieTable() {const [currentPage, setCurrentPage] = useState(1);const [data, setData] = useState<Movie[]>(() => movies.slice(0, pageSize));return (<TableContainer><Table aria-label="Highest-rated movies"><TableHeader><TableHeaderRow>// ...</TableHeaderRow></TableHeader><TableBody>{movies.map((movie) => ({data.map((movie) => (<TableBodyRow key={movie.title}>// ...</TableBodyRow>))}</TableBody></Table></TableContainer>);} -
Add a pagination component. Don’t forget to link it with the table so that screen readers can recognize the relationship.
table.tsx // ...const pageSize = 3;const pageCount = Math.floor(movies.length / 3) + 1;export function MovieTable() {const [currentPage, setCurrentPage] = useState(1);const [data, setData] = useState<Movie[]>(() => movies.slice(0, pageSize));const handlePageChange = (page: number) => {setCurrentPage(page);setData(movies.slice((page - 1) * pageSize, page * pageSize));};return (<Stack gap={16}><TableContainer><Table aria-label="Highest-rated movies"><Table id="movie-table" aria-label="Highest-rated movies"><TableHeader><TableHeaderRow>// ...</TableHeaderRow></TableHeader><TableBody>{data.map((movie) => (<TableBodyRow key={movie.title}>// ...</TableBodyRow>))}</TableBody></Table></TableContainer><Paginationaria-label="Table pagination"paginatedElementId="movie-table"pageCount={pageCount}currentPage={currentPage}onPageChange={handlePageChange}/></Stack>);} -
Provide
totalRowCount
androwNumber
props. Make sure to include header rows in the calculation. Refer to the table API reference for more information on these props.table.tsx // ...const pageSize = 3;const pageCount = Math.floor(movies.length / 3) + 1;export function MovieTable() {const [currentPage, setCurrentPage] = useState(1);const [data, setData] = useState<Movie[]>(() => movies.slice(0, pageSize));const handlePageChange = (page: number) => {setCurrentPage(page);setData(movies.slice((page - 1) * pageSize, page * pageSize));};return (<Stack gap={16}><TableContainer><Table id="movie-table" aria-label="Highest-rated movies"><Table id="movie-table" aria-label="Highest-rated movies" totalRowCount={movies.length + 1}><TableHeader><TableHeaderRow><TableHeaderRow rowNumber={1}>// ...</TableHeaderRow></TableHeader><TableBody>{data.map((movie) => (<TableBodyRow key={movie.title}><TableBodyRow key={movie.title} rowNumber={movies.indexOf(movie) + 2}>// ...</TableBodyRow>))}</TableBody></Table></TableContainer><Paginationaria-label="Table pagination"paginatedElementId="movie-table"pageCount={pageCount}currentPage={currentPage}onPageChange={handlePageChange}/></Stack>);}
-
Enable pagination in the data engine. Consult the TanStack Table documentation for details.
table.tsx import { Table, useTableEngine } from '@cimpress-ui/tanstack-table-react';import { getCoreRowModel } from '@cimpress-ui/tanstack-table-react/base';import { getCoreRowModel, getPaginationRowModel } from '@cimpress-ui/tanstack-table-react/base';import { columns } from './columns.js';import { type Movie, movies } from './data.js';export function MovieTable() {const engine = useTableEngine<Movie>({columns,data: movies,getCoreRowModel: getCoreRowModel(),getPaginationRowModel: getPaginationRowModel(),initialState: {pagination: {pageSize: 3,},},});return <Table engine={engine} aria-label="Highest-rated movies" />;} -
Add a pagination component. Don’t forget to link it with the table so that screen readers can recognize the relationship.
table.tsx import { Stack } from '@cimpress-ui/react';import { Table, useTableEngine } from '@cimpress-ui/tanstack-table-react';import { Table, TablePagination, useTableEngine } from '@cimpress-ui/tanstack-table-react';import { getCoreRowModel, getPaginationRowModel } from '@cimpress-ui/tanstack-table-react/base';import { columns } from './columns.js';import { type Movie, movies } from './data.js';export function MovieTable() {const engine = useTableEngine<Movie>({columns,data: movies,getCoreRowModel: getCoreRowModel(),getPaginationRowModel: getPaginationRowModel(),initialState: {pagination: {pageSize: 3,},},});return <Table engine={engine} aria-label="Highest-rated movies" />;return (<Stack gap={16}><Table engine={engine} id="movie-table" aria-label="Highest-rated movies" /><TablePagination engine={engine} aria-label="Table pagination" paginatedElementId="movie-table" /></Stack>);} -
Provide
totalRowCount
andgetRowNumber
to engine options. Make sure to include header rows in the calculation.table.tsx import { Stack } from '@cimpress-ui/react';import { Table, TablePagination, useTableEngine } from '@cimpress-ui/tanstack-table-react';import { getCoreRowModel, getPaginationRowModel } from '@cimpress-ui/tanstack-table-react/base';import { columns } from './columns.js';import { type Movie, movies } from './data.js';export function MovieTable() {const engine = useTableEngine<Movie>({columns,data: movies,getCoreRowModel: getCoreRowModel(),getPaginationRowModel: getPaginationRowModel(),initialState: {pagination: {pageSize: 3,},},cimpressUi: {totalRowCount: movies.length + 1,getRowNumber: (row) => movies.indexOf(row) + 2,},});return (<Stack gap={16}><Table engine={engine} id="movie-table" aria-label="Highest-rated movies" /><TablePagination engine={engine} aria-label="Table pagination" paginatedElementId="movie-table" /></Stack>);}
Here is how our paginated table looks like. You can verify the presence of additional row-related attributes by inspecting the DOM.
Title | Release year | Rating |
---|---|---|
The Shawshank Redemption | 1994 | 9.3 |
The Godfather | 1972 | 9.2 |
The Godfather: Part II | 1974 | 9.0 |
Subset of columns
Section titled “Subset of columns”If your table presents a subset of columns from a larger dataset (such as when the user is allowed to hide columns), you should provide the following additional information:
- The number of columns in the full dataset.
- The position of each column within the full dataset.
Let’s hide the “Release year” column from our table:
-
Remove the “Release year” column from the table markup.
table.tsx // ...export function MovieTable() {return (<TableContainer><Table aria-label="Highest-rated movies"><TableHeader><TableHeaderRow><TableHeaderCell columnKey="title" columnWidth="60%">Title</TableHeaderCell><TableHeaderCell columnKey="release-year" columnWidth="20%" columnContentAlignment="end">Release year</TableHeaderCell><TableHeaderCell columnKey="rating" columnWidth="20%" columnContentAlignment="end">Rating</TableHeaderCell></TableHeaderRow></TableHeader><TableBody>{movies.map((movie) => (<TableBodyRow key={movie.title}><TableBodyCell columnKey="title">{movie.title}</TableBodyCell><TableBodyCell columnKey="release-year">{movie.releaseYear}</TableBodyCell><TableBodyCell columnKey="rating"><span aria-hidden>⭐️ </span>{movie.rating.toFixed(1)}</TableBodyCell></TableBodyRow>))}</TableBody></Table></TableContainer>);} -
Provide
totalColumnCount
andcolumnNumber
props. Refer to the table API reference for more information on these props.table.tsx // ...export function MovieTable() {return (<TableContainer><Table aria-label="Highest-rated movies"><Table aria-label="Highest-rated movies" totalColumnCount={3}><TableHeader><TableHeaderRow><TableHeaderCellcolumnKey="title"columnNumber={1}columnWidth="60%">Title</TableHeaderCell><TableHeaderCellcolumnKey="rating"columnNumber={3}columnWidth="20%"columnContentAlignment="end">Rating</TableHeaderCell></TableHeaderRow></TableHeader><TableBody>// ...</TableBody></Table></TableContainer>);}
-
Enable column hiding in the data engine. Consult the TanStack Table documentation for details.
table.tsx import { Table, useTableEngine } from '@cimpress-ui/tanstack-table-react';import { getCoreRowModel } from '@cimpress-ui/tanstack-table-react/base';import { columns } from './columns.js';import { type Movie, movies } from './data.js';export function MovieTable() {const engine = useTableEngine<Movie>({columns,data: movies,getCoreRowModel: getCoreRowModel(),enableHiding: true,});return <Table engine={engine} aria-label="Highest-rated movies" />;} -
Mark the “Release year” column as hidden.
table.tsx import { Table, useTableEngine } from '@cimpress-ui/tanstack-table-react';import { getCoreRowModel } from '@cimpress-ui/tanstack-table-react/base';import { columns } from './columns.js';import { type Movie, movies } from './data.js';export function MovieTable() {const engine = useTableEngine<Movie>({columns,data: movies,getCoreRowModel: getCoreRowModel(),enableHiding: true,state: {columnVisibility: {'release-year': false,},},});return <Table engine={engine} aria-label="Highest-rated movies" />;} -
That’s it! All required accessibility properties will be set automatically since the engine is aware of all columns.
Here is how our table looks like. You can verify the presence of additional column-related attributes by inspecting the DOM.
Title | Rating |
---|---|
The Shawshank Redemption | 9.3 |
The Godfather | 9.2 |
The Godfather: Part II | 9.0 |
The Dark Knight | 9.0 |
12 Angry Men | 8.9 |
Schindler's List | 8.9 |
The Lord of the Rings: The Return of the King | 8.9 |
Pulp Fiction | 8.9 |
The Lord of the Rings: The Fellowship of the Ring | 8.8 |
Forrest Gump | 8.8 |
No data
Section titled “No data”It’s possible for the table to have no rows to display. This can happen when no data is available, or when the table rows are filtered. Let’s see how we can display an “empty state” in our table if needed.
Let’s assume our table of highest-rated movies has been filtered so that no rows meet the filtering criteria. We’re not going to set up the filtering itself in this guide. Instead, in order to keep things simple, we’ll assume that the movies
array can be empty.
All we need to do is render the <TableEmptyState>
component when there’s no rows to display, like so:
import { Button, Table, TableBody, TableBodyCell, TableBodyRow, TableContainer, TableEmptyState, TableHeader, TableHeaderCell, TableHeaderRow,} from '@cimpress-ui/react';
// ...
export function MovieTable() { return ( <TableContainer> <Table aria-label="Highest-rated movies"> <TableHeader> <TableHeaderRow> // ... </TableHeaderRow> </TableHeader> <TableBody> {movies.map((movie) => ( {movies.length > 0 ? movies.map((movie) => ( <TableBodyRow key={movie.title}> // ... </TableBodyRow> ))} )) : ( <TableEmptyState title="No movies to display" description="Try adjusting or clearing your filters." action={<Button size="small">Clear filters</Button>} /> )} </TableBody> </Table> </TableContainer> );}
All we need to do is provide the empty state options to the useTableEngine
hook, like so:
import { Button } from '@cimpress-ui/react';import { Table, useTableEngine } from '@cimpress-ui/tanstack-table-react';import { getCoreRowModel } from '@cimpress-ui/tanstack-table-react/base';import { columns } from './columns.js';import { type Movie, movies } from './data.js';
export function MovieTable() { const engine = useTableEngine<Movie>({ columns, data: movies, getCoreRowModel: getCoreRowModel(), cimpressUi: { emptyState: { title: 'No movies to display', description: 'Try adjusting or clearing your filters.', action: <Button size="small">Clear filters</Button>, }, }, });
return <Table engine={engine} aria-label="Highest-rated movies" />;}
Here is the finished table with no data to display:
Title | Release year | Rating |
---|---|---|
No movies to displayTry adjusting or clearing your filters. |
Row selection
Section titled “Row selection”Often we want users to be able to select specific rows in a table. Cimpress UI provides building blocks for row selection functionality, but you are in control of the selection state.
-
Define state required for row selection.
table.tsx // ...export function MovieTable() {const [selectedRows, setSelectedRows] = useState<Record<number, true>>({});const [areAllRowsSelected, setAreAllRowsSelected] = useState(false);return (// ...);} -
Connect your state with table components.
table.tsx // ...export function MovieTable() {const [selectedRows, setSelectedRows] = useState<Record<number, true>>({});const [areAllRowsSelected, setAreAllRowsSelected] = useState(false);return (<TableContainer><Table aria-label="Highest-rated movies"><Tablearia-label="Highest-rated movies"rowSelectionMode="multiple"areAllRowsSelected={areAllRowsSelected}areSomeRowsSelected={areAllRowsSelected || Object.keys(selectedRows).length > 0}onAllRowsSelectionToggle={(value) => setAreAllRowsSelected(value ?? !areAllRowsSelected)}><TableHeader>// ...</TableHeader><TableBody>{movies.map((movie, idx) => (<TableBodyRow key={movie.title}><TableBodyRowkey={movie.title}isSelected={areAllRowsSelected || idx in selectedRows}onSelectionToggle={(value) =>setSelectedRows((prev) => {if ((value === undefined && prev[idx]) || value === false) {delete prev[idx];return { ...prev };} else {return { ...prev, [idx]: true };}})}>// ...</TableBodyRow>))}</TableBody></Table></TableContainer>);} -
Add a selection column to your table, so that users have a visible indicator of which rows are selected.
table.tsx import {Table,TableAllRowsSelector,TableBody,TableBodyCell,TableBodyRow,TableContainer,TableHeader,TableHeaderCell,TableHeaderRow,TableRowSelector,} from '@cimpress-ui/react';import { movies } from './data.js';export function MovieTable() {const [selectedRows, setSelectedRows] = useState<Record<number, true>>({});const [areAllRowsSelected, setAreAllRowsSelected] = useState(false);return (// ...<TableHeader><TableHeaderRow><TableHeaderCell columnKey="row-selector" columnWidth="0" columnContentAlignment="center"><TableAllRowsSelector aria-label="Select all rows" /></TableHeaderCell>// ...</TableHeaderRow></TableHeader><TableBody>{movies.map((movie) => ({movies.map((movie, idx) => (<TableBodyRowkey={movie.title}isSelected={areAllRowsSelected || idx in selectedRows}onSelectionToggle={(value) =>setSelectedRows((prev) => {if ((value === undefined && prev[idx]) || value === false) {delete prev[idx];return { ...prev };} else {return { ...prev, [idx]: true };}})}><TableBodyCell columnKey="row-selector"><TableRowSelector aria-label={`Select row ${idx + 2}`} /></TableBodyCell>// ...</TableBodyRow>))}</TableBody>// ...);}
-
Enable row selection in the data engine. Consult the TanStack Table documentation for details.
table.tsx import { Table, useTableEngine } from '@cimpress-ui/tanstack-table-react';import { getCoreRowModel } from '@cimpress-ui/tanstack-table-react/base';import { columns } from './columns.js';import { type Movie, movies } from './data.js';export function MovieTable() {const engine = useTableEngine<Movie>({columns,data: movies,getCoreRowModel: getCoreRowModel(),enableRowSelection: true,enableMultiRowSelection: true,});return <Table engine={engine} aria-label="Highest-rated movies" />;} -
Add a selection column to your column definitions, so that users have a visible indicator of which rows are selected.
columns.ts import { TableAllRowsSelector, TableRowSelector } from '@cimpress-ui/react';import { createColumnHelper } from '@cimpress-ui/tanstack-table-react/base';import type { Movie } from './data.js';const columnHelper = createColumnHelper<Movie>();export const columns = [columnHelper.display({id: 'row-selector',header: () => <TableAllRowsSelector aria-label="Select all rows" />,cell: ({ row }) => <TableRowSelector aria-label={`Select row ${row.index + 2}`} />,width: 0,contentAlignment: 'center',}),// ...];
Here is the finished table with selectable rows:
Title | Release year | Rating | |
---|---|---|---|
The Shawshank Redemption | 1994 | 9.3 | |
The Godfather | 1972 | 9.2 | |
The Godfather: Part II | 1974 | 9.0 | |
The Dark Knight | 2008 | 9.0 | |
12 Angry Men | 1957 | 8.9 | |
Schindler's List | 1993 | 8.9 | |
The Lord of the Rings: The Return of the King | 2003 | 8.9 | |
Pulp Fiction | 1994 | 8.9 | |
The Lord of the Rings: The Fellowship of the Ring | 2001 | 8.8 | |
Forrest Gump | 1994 | 8.8 |
Column sorting
Section titled “Column sorting”Cimpress UI’s table header cells have sorting capabilities built-in. As with other functionalities, you are in control of the sort state.
-
Define state required for column sorting.
table.tsx import {Table,TableBody,TableBodyCell,TableBodyRow,TableColumnSortOrder,TableContainer,TableHeader,TableHeaderCell,TableHeaderRow,} from '@cimpress-ui/react';import { movies } from './data.js';export function MovieTable() {const [sortedColumnKey, setSortedColumnKey] = useState<string>('rating');const [sortOrder, setSortOrder] = useState<TableColumnSortOrder>('descending');return (// ...);} -
Connect your state with table components.
table.tsx // ...export function MovieTable() {const [sortedColumnKey, setSortedColumnKey] = useState<string>('rating');const [sortOrder, setSortOrder] = useState<TableColumnSortOrder>('descending');const handleSortOrderChange = (columnKey: string) => {if (columnKey !== sortedColumnKey) {setSortedColumnKey(columnKey);setSortOrder('ascending');} else if (sortOrder === 'ascending') {setSortOrder('descending');} else {setSortedColumnKey('');}};return (<TableContainer><Table aria-label="Highest-rated movies"><TableHeader><TableHeaderRow><TableHeaderCellcolumnKey="title"columnWidth="60%"sortOrder={sortedColumnKey === 'title' ? sortOrder : undefined}onSortOrderToggle={() => handleSortOrderChange('title')}>Title</TableHeaderCell><TableHeaderCellcolumnKey="release-year"columnWidth="20%"columnContentAlignment="end"sortOrder={sortedColumnKey === 'release-year' ? sortOrder : undefined}onSortOrderToggle={() => handleSortOrderChange('release-year')}>Release year</TableHeaderCell><TableHeaderCellcolumnKey="rating"columnWidth="20%"columnContentAlignment="end"sortOrder={sortedColumnKey === 'rating' ? sortOrder : undefined}onSortOrderToggle={() => handleSortOrderChange('rating')}>Rating</TableHeaderCell></TableHeaderRow></TableHeader><TableBody>{movies.map((movie, idx) => (// ...))}</TableBody></Table></TableContainer>);}
-
Enable column sorting in the data engine. Consult the TanStack Table documentation for details.
table.tsx import { Table, useTableEngine } from '@cimpress-ui/tanstack-table-react';import { getCoreRowModel } from '@cimpress-ui/tanstack-table-react/base';import { getCoreRowModel, getSortedRowModel } from '@cimpress-ui/tanstack-table-react/base';import { columns } from './columns.js';import { type Movie, movies } from './data.js';export function MovieTable() {const engine = useTableEngine<Movie>({columns,data: movies,getCoreRowModel: getCoreRowModel(),getSortedRowModel: getSortedRowModel(),enableSorting: true,enableSortingRemoval: true,initialState: {sorting: [{ id: 'rating', desc: true }],},});return <Table engine={engine} aria-label="Highest-rated movies" />;} -
That’s it! All required accessibility properties will be set automatically.
Here is the finished table with sortable columns:
Title | Release year | Rating |
---|---|---|
The Shawshank Redemption | 1994 | 9.3 |
The Godfather | 1972 | 9.2 |
The Godfather: Part II | 1974 | 9.0 |
The Dark Knight | 2008 | 9.0 |
12 Angry Men | 1957 | 8.9 |
Schindler's List | 1993 | 8.9 |
The Lord of the Rings: The Return of the King | 2003 | 8.9 |
Pulp Fiction | 1994 | 8.9 |
The Lord of the Rings: The Fellowship of the Ring | 2001 | 8.8 |
Forrest Gump | 1994 | 8.8 |