Table
Table is a table component that groups content that is similar or related in a grid-like format across rows and columns. They organise information in way that's easy to scan, so that users can look for patterns and insights.
Experimental but encouraged!
Our updated table component has been flagged as "experimental" due to the recent move to a "semi-headless" approach. Its use is encouraged, as is any feedback you can provide about the way the component works, the API design or any other concerns.
We are leveraging the open source package react-table
which does much of the heavy lifting for us. react-table
is a "headless UI" package which means it exports no components, only hooks. You must bring your own markup to use react-table
. This may sound like additional work, and it is, but it means that it is extremely powerful and flexible, supporting a vast array of dynamic table functionality:
- Sorting
- Filtering
- Pagination
- Row Expansion
- Row Selection
- ...and much more!
React-table supports almost any kind of data table you can imagine. Take a look at their examples to see.
Our Mesh table provides the UI pieces for you to put together the markup for your react-table
, as well as some small abstractions where it makes sense.
As such, you will need to install this package, but also the @tanstack/react-table
directly in your solution.
Installation
npm install @nib/table @tanstack/react-table
Note that we are also installing
@tanstack/react-table
separately, as it is not included as a dependency. Note: You will also need to install the peerDependencies @nib/icons and @nib-components/theme.
Usage
We export the following components for use with react-table
:
Component | Description |
---|---|
TableWrapper | optional outer wrapper to control horizontal overflow |
TableCaption | optional caption above or below the table |
Table | the table element |
TableHead | the thead element |
TableHeadRow | the tr element for use within the TableHead |
TableHeading | a th element, with prebuilt functionality to support sorting |
Th | a static th element |
TableBody | the tbody element |
TableRow | the tr element |
TableData | the td element |
TableExpandHeading | an empty th element for use in expanding tables |
TableExpandRow | the tr element for use in expanding rows |
TableExpandData | the td element containing the button and chevron for expanding tables |
IndeterminateCheckbox | a checkbox for use with a row selecting table |
Pagination | A collection of pagination controls |
SearchBox | A simple search form for a global filter |
SimpleTable | (deprecated) |
Basic
Static
For static tables our table elements (Table
, TableHead
, TableBody
, TableRow
, TableData
, etc.) can be used directly in your markup:
import {Table, TableHead, TableHeadRow, TableHeading, TableBody, TableRow, TableData} from '@nib/table';<Table><TableHead><TableHeadRow><TableHeading>Column 1</TableHeading><TableHeading>Column 2</TableHeading></TableHeadRow></TableHead><TableBody><TableRow><TableData>Sample data</TableData><TableData>Sample data</TableData></TableRow></TableBody></Table>;
Dynamic
For more dynamic data, the Mesh Table components are to be used in conjunction with the useReactTable
hook from @tanstack/react-table
.
At this point it is a good idea to familiarise yourself with Column Defs. As the docs state "Column defs are the single most important part of building a table." and are responsible for the building the underlying data model that will be used for everything including sorting, filtering, grouping, etc.
column1 | column2 | column3 | column4 | column5 | column6 |
---|---|---|---|---|---|
Column 1 | Column 2 | Column 3 | Column 4 | Column 5 | Column 6 |
Column 1 | Column 2 | Column 3 | Column 4 | Column 5 | Column 6 |
Lorem Ipsum is simply dummy text | Column 2 | Column 3 | Column 4 | Column 5 | Column 6 |
Column 1 | Column 2 | Column 3 | Column 4 | Column 5 | Column 6 |
Column 1 | Column 2 | Column 3 | Column 4 | Column 5 | Column 6 |
import React from 'react';import {flexRender, getCoreRowModel, createColumnHelper, useReactTable} from '@tanstack/react-table';import {TableWrapper, TableCaption, Table, TableHead, TableHeadRow, TableHeading, TableBody, TableRow, TableData} from '@nib/table';const validRowHeightValues = ['relaxed', 'regular', 'condensed'] as const;type rowHeightValues = (typeof validRowHeightValues)[number];const validCaptionSideValues = ['top', 'bottom'] as const;type captionSideValues = (typeof validCaptionSideValues)[number];export interface BasicTableProps {caption?: string;captionSide?: captionSideValues;rowHeight?: rowHeightValues;height?: string;maxHeight?: string;rowHover?: boolean;stripedRows?: boolean;equalColumns?: boolean;fixedHeader?: boolean;fixedColumn?: boolean;[key: string]: unknown;}type Row = {column1: string;column2: string;column3: string;column4: string;column5: string;column6: string;}export const BasicTable: React.FC<BasicTableProps> = ({caption,captionSide,stripedRows,rowHover,height,maxHeight,equalColumns,rowHeight,fixedHeader,fixedColumn,...otherProps}) => {const columnHelper = createColumnHelper<Row>();const columns = [columnHelper.accessor('column1', {cell: info => info.getValue(),footer: info => info.column.id}),columnHelper.accessor('column2', {cell: info => info.getValue(),footer: info => info.column.id}),columnHelper.accessor('column3', {cell: info => info.getValue(),footer: info => info.column.id}),columnHelper.accessor('column4', {cell: info => info.getValue(),footer: info => info.column.id}),columnHelper.accessor('column5', {cell: info => info.getValue(),footer: info => info.column.id}),columnHelper.accessor('column6', {cell: info => info.getValue(),footer: info => info.column.id})];const data = [{column1: 'Column 1',column2: 'Column 2',column3: 'Column 3',column4: 'Column 4',column5: 'Column 5',column6: 'Column 6'},{column1: 'Column 1',column2: 'Column 2',column3: 'Column 3',column4: 'Column 4',column5: 'Column 5',column6: 'Column 6'},{column1: 'Lorem Ipsum is simply dummy text ',column2: 'Column 2',column3: 'Column 3',column4: 'Column 4',column5: 'Column 5',column6: 'Column 6'},{column1: 'Column 1',column2: 'Column 2',column3: 'Column 3',column4: 'Column 4',column5: 'Column 5',column6: 'Column 6'},{column1: 'Column 1',column2: 'Column 2',column3: 'Column 3',column4: 'Column 4',column5: 'Column 5',column6: 'Column 6'}];const table = useReactTable({data,columns,getCoreRowModel: getCoreRowModel()});return (<TableWrapper height={height} maxHeight={maxHeight}><Table {...otherProps} equalColumns={equalColumns}>{caption && <TableCaption captionSide={captionSide}>{caption}</TableCaption>}<TableHead>{table.getHeaderGroups().map((headerGroup, index) => (<TableHeadRow key={`header-group-${index}`} fixedHeader={fixedHeader}>{headerGroup.headers.map(header => (<TableHeading key={header.id} fixedColumn={fixedColumn}>{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}</TableHeading>))}</TableHeadRow>))}</TableHead><TableBody stripedRows={stripedRows} rowHover={rowHover}>{table.getRowModel().rows.map(row => (<TableRow key={row.id} rowHeight={rowHeight}>{row.getVisibleCells().map(cell => (<TableData key={cell.id} fixedColumn={fixedColumn}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableData>))}</TableRow>))}</TableBody></Table></TableWrapper>);};
Sorting
Table, in conjunction with react-table
, supports sorting data by a single column. Columns can be sorted in ascending or descending order, or reverted to its unsorted state, by clicking on the the column header. The below code snippet provides an example usage of sorting functionality.
For more examples and detailed documentation:
- React Table Sorting API Docs
- React Table Sorting Examples
- Sorting example in storybook, opens in a new tab
Kaley | Pollich | 12 | 563 | single | 58 | 2001-10-22T09:20:21.683Z |
Lurline | Kerluke | 20 | 877 | complicated | 14 | 1995-09-28T06:49:17.599Z |
Raquel | Kuhic | 15 | 939 | single | 91 | 1993-12-12T02:42:59.627Z |
Vivienne | Schuppe | 34 | 807 | relationship | 49 | 1991-02-08T13:16:13.271Z |
Isabella | Feeney | 10 | 979 | relationship | 31 | 2013-09-07T06:27:50.422Z |
Harold | Keebler | 33 | 500 | complicated | 93 | 1994-11-01T21:55:06.822Z |
Kali | McClure | 17 | 164 | complicated | 77 | 1999-04-27T05:42:43.256Z |
Nichole | Daugherty | 40 | 495 | complicated | 82 | 2013-04-01T19:24:32.137Z |
Dixie | Bayer | 14 | 478 | complicated | 34 | 2012-09-08T18:07:26.336Z |
Bruce | Macejkovic | 34 | 43 | relationship | 99 | 2013-07-08T21:34:36.296Z |
import React from 'react';import {ColumnDef, flexRender, getCoreRowModel, getSortedRowModel, SortingState, useReactTable} from '@tanstack/react-table';import {TableWrapper, Table, TableHead, TableHeadRow, TableHeading, TableBody, TableRow, TableData} from '@nib/table';type Person = {firstName: string;lastName: string;age: number;visits: number;progress: number;subRows?: Person[];};const data = [{firstName: 'Kaley',lastName: 'Pollich',age: 12,visits: 563,progress: 58},{firstName: 'Lurline',lastName: 'Kerluke',age: 20,visits: 877,progress: 14}];export const SortingTable = () => {const [sorting, setSorting] = React.useState < SortingState > [];const columns = React.useMemo<ColumnDef<Person>[]>(() => [{accessorKey: 'firstName',cell: info => info.getValue(),footer: props => props.column.id,header: () => <span>First Name</span>},{accessorFn: row => row.lastName,id: 'lastName',cell: info => info.getValue(),header: () => <span>Last Name</span>,footer: props => props.column.id},{accessorKey: 'age',header: () => 'Age',footer: props => props.column.id},{accessorKey: 'visits',header: () => <span>Visits</span>,footer: props => props.column.id},{accessorKey: 'progress',header: 'Profile Progress',footer: props => props.column.id}],[]);const table = useReactTable({data,columns,state: {sorting},onSortingChange: setSorting,getCoreRowModel: getCoreRowModel(),getSortedRowModel: getSortedRowModel(),debugTable: true});return (<TableWrapper><Table><TableHead>{table.getHeaderGroups().map(headerGroup => (<TableHeadRow key={headerGroup.id}>{headerGroup.headers.map(header => {return (<TableHeadingkey={header.id}colSpan={header.colSpan}canColumnSort={header.column.getCanSort()}onClick={header.column.getToggleSortingHandler()}isSorted={header.column.getIsSorted()}>{header.isPlaceholder ? null : <span>{flexRender(header.column.columnDef.header, header.getContext())}</span>}</TableHeading>);})}</TableHeadRow>))}</TableHead><TableBody>{table.getRowModel().rows.slice(0, 10).map(row => {return (<TableRow key={row.id}>{row.getVisibleCells().map(cell => {return <TableData key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableData>;})}</TableRow>);})}</TableBody></Table></TableWrapper>);};
Row Selection
To add row selection to your DataTable, you need to add both a useState
hook to enable selectable rows and a onRowSelectionChange
property to the useReactTable
hook.
For more examples and detailed documentation:
- React Table Row Selection API Docs
- React Table Row Selection Examples
- Row Selection example in storybook, opens in a new tab
First Name | Last Name | Age | Visits | Status | Profile Progress | |
---|---|---|---|---|---|---|
Kaley | Pollich | 12 | 563 | single | 58 | |
Lurline | Kerluke | 20 | 877 | complicated | 14 | |
Raquel | Kuhic | 15 | 939 | single | 91 | |
Vivienne | Schuppe | 34 | 807 | relationship | 49 | |
Isabella | Feeney | 10 | 979 | relationship | 31 | |
Harold | Keebler | 33 | 500 | complicated | 93 | |
Kali | McClure | 17 | 164 | complicated | 77 | |
Nichole | Daugherty | 40 | 495 | complicated | 82 | |
Dixie | Bayer | 14 | 478 | complicated | 34 | |
Bruce | Macejkovic | 34 | 43 | relationship | 99 |
import React from 'react';import {ColumnDef, flexRender, getCoreRowModel, useReactTable} from '@tanstack/react-table';import {TableWrapper, Table, TableHead, TableHeadRow, TableHeading, TableBody, TableRow, TableData, IndeterminateCheckbox} from '@nib/table';type Person = {firstName: string;lastName: string;age: number;visits: number;progress: number;subRows?: Person[];};const data = [{firstName: 'Kaley',lastName: 'Pollich',age: 12,visits: 563,progress: 58},{firstName: 'Lurline',lastName: 'Kerluke',age: 20,visits: 877,progress: 14}];export const RowSelectionTable = () => {const [rowSelection, setRowSelection] = React.useState({});const tableId = React.useId();const columns = React.useMemo<ColumnDef<Person>[]>(() => [{id: 'select',header: ({table}) => (<IndeterminateCheckbox{...{id: tableIdchecked: table.getIsAllRowsSelected(),indeterminate: table.getIsSomeRowsSelected(),onChange: table.getToggleAllRowsSelectedHandler()}}/>),cell: ({row}) => (<div><IndeterminateCheckbox{...{id: `row-${row.id}-${tableId}`,checked: row.getIsSelected(),indeterminate: row.getIsSomeSelected(),onChange: row.getToggleSelectedHandler()}}/></div>)},{accessorKey: 'firstName',cell: info => info.getValue(),footer: props => props.column.id,header: () => <span>First Name</span>,enableColumnFilter: false},{accessorFn: row => row.lastName,enableColumnFilter: false,id: 'lastName',cell: info => info.getValue(),header: () => <span>Last Name</span>,footer: props => props.column.id},{accessorKey: 'age',enableColumnFilter: false,header: () => 'Age',footer: props => props.column.id},{accessorKey: 'visits',enableColumnFilter: false,header: () => <span>Visits</span>,footer: props => props.column.id},{accessorKey: 'status',enableColumnFilter: false,header: 'Status',footer: props => props.column.id},{accessorKey: 'progress',enableColumnFilter: false,header: 'Profile Progress',footer: props => props.column.id}],[]);const table = useReactTable({data,columns,state: {rowSelection},onRowSelectionChange: setRowSelection,getCoreRowModel: getCoreRowModel(),debugTable: true});return (<div><TableWrapper><Table><TableHead>{table.getHeaderGroups().map(headerGroup => (<TableHeadRow key={headerGroup.id}>{headerGroup.headers.map(header => {return (<TableHeading key={header.id} colSpan={header.colSpan}>{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}</TableHeading>);})}</TableHeadRow>))}</TableHead><TableBody>{table.getRowModel().rows.map(row => {return (<TableRow key={row.id}>{row.getVisibleCells().map(cell => {return <TableData key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableData>;})}</TableRow>);})}</TableBody></Table></TableWrapper><div>{Object.keys(rowSelection).length} of {table.getPreFilteredRowModel().rows.length} Total Rows Selected</div></div>);};
Pagination
For pagination, we export a standard Pagination
component with props compatible with react-table
.
In the above demo we have used a local state to manage the page information, but in a real table you would use getPaginationRowModel
in the useReactTable
hook - demonstrated below.
For more examples and detailed documentation:
- React Table Pagination API Docs
- React Table Pagination Examples
- Pagination example in storybook, opens in a new tab
First Name | Last Name | Age | Visits | Status | Profile Progress | Created At |
---|---|---|---|---|---|---|
Kaley | Pollich | 12 | 563 | single | 58 | 2001-10-22T09:20:21.683Z |
Lurline | Kerluke | 20 | 877 | complicated | 14 | 1995-09-28T06:49:17.599Z |
Raquel | Kuhic | 15 | 939 | single | 91 | 1993-12-12T02:42:59.627Z |
Vivienne | Schuppe | 34 | 807 | relationship | 49 | 1991-02-08T13:16:13.271Z |
Isabella | Feeney | 10 | 979 | relationship | 31 | 2013-09-07T06:27:50.422Z |
Harold | Keebler | 33 | 500 | complicated | 93 | 1994-11-01T21:55:06.822Z |
Kali | McClure | 17 | 164 | complicated | 77 | 1999-04-27T05:42:43.256Z |
Nichole | Daugherty | 40 | 495 | complicated | 82 | 2013-04-01T19:24:32.137Z |
Dixie | Bayer | 14 | 478 | complicated | 34 | 2012-09-08T18:07:26.336Z |
Bruce | Macejkovic | 34 | 43 | relationship | 99 | 2013-07-08T21:34:36.296Z |
import React from 'react';import {ColumnDef, flexRender, getCoreRowModel, useReactTable, getPaginationRowModel} from '@tanstack/react-table';import {TableWrapper, Table, TableHead, TableHeadRow, TableHeading, TableBody, TableRow, TableData, Pagination} from '@nib/table';type Person = {firstName: string;lastName: string;age: number;visits: number;progress: number;subRows?: Person[];};const data = [{firstName: 'Kaley',lastName: 'Pollich',age: 12,visits: 563,progress: 58},{firstName: 'Lurline',lastName: 'Kerluke',age: 20,visits: 877,progress: 14,status: 'complicated'},{firstName: 'Raquel',lastName: 'Kuhic',age: 15,visits: 939,progress: 91},{firstName: 'Vivienne',lastName: 'Schuppe',age: 34,visits: 807,progress: 49,status: 'relationship'},{firstName: 'Isabella',lastName: 'Feeney',age: 10,visits: 979,progress: 31,status: 'relationship'},{firstName: 'Harold',lastName: 'Keebler',age: 33,visits: 500,progress: 93,status: 'complicated'},{firstName: 'Kali',lastName: 'McClure',age: 17,visits: 164,progress: 77,status: 'complicated'},{firstName: 'Nichole',lastName: 'Daugherty',age: 40,visits: 495,progress: 82,status: 'complicated'},{firstName: 'Dixie',lastName: 'Bayer',age: 14,visits: 478,progress: 34,status: 'complicated'},{firstName: 'Bruce',lastName: 'Macejkovic',age: 34,visits: 43,progress: 99,status: 'relationship'},{firstName: 'Pietro',lastName: 'Koss',age: 6,visits: 624,progress: 60},{firstName: 'Karen',lastName: 'Torphy',age: 18,visits: 21,progress: 51},{firstName: 'Marco',lastName: 'Abernathy',age: 11,visits: 325,progress: 80,status: 'complicated'},{firstName: 'Lelah',lastName: 'Bradtke',age: 16,visits: 589,progress: 38,status: 'complicated'},{firstName: 'Meda',lastName: 'Thiel',age: 22,visits: 767,progress: 58,status: 'relationship'},{firstName: 'Elda',lastName: 'Berge',age: 7,visits: 639,progress: 3,status: 'complicated'},{firstName: 'Lucy',lastName: 'Block',age: 30,visits: 970,progress: 98,status: 'relationship'},{firstName: 'Adan',lastName: 'Mante',age: 12,visits: 326,progress: 77},{firstName: 'Patricia',lastName: 'Friesen',age: 11,visits: 954,progress: 75,status: 'relationship'},{firstName: 'Katheryn',lastName: 'Walter',age: 11,visits: 558,progress: 33,status: 'relationship'},{firstName: 'Adelle',lastName: 'Armstrong',age: 13,visits: 560,progress: 47,status: 'relationship'},{firstName: 'Mose',lastName: 'Rowe',age: 18,visits: 198,progress: 42,status: 'relationship'},{firstName: 'Itzel',lastName: 'Fritsch',age: 17,visits: 193,progress: 28,status: 'complicated'},{firstName: 'Reinhold',lastName: 'Wiza',age: 17,visits: 390,progress: 67,status: 'relationship'},{firstName: 'Giuseppe',lastName: 'Turner',age: 14,visits: 111,progress: 59},{firstName: 'Annalise',lastName: 'Barrows',age: 3,visits: 837,progress: 89,status: 'relationship'},{firstName: 'Lilliana',lastName: 'Donnelly',age: 12,visits: 755,progress: 83,status: 'complicated'},{firstName: 'Monique',lastName: 'Klein',age: 8,visits: 70,progress: 72},{firstName: 'Leland',lastName: 'Halvorson',age: 28,visits: 388,progress: 4},{firstName: 'Theresia',lastName: 'Stroman',age: 26,visits: 614,progress: 61,status: 'complicated'}];export const PaginationTable = () => {const columns = React.useMemo<ColumnDef<Person>[]>(() => [{accessorKey: 'firstName',cell: info => info.getValue(),footer: props => props.column.id,header: () => <span>First Name</span>},{accessorFn: row => row.lastName,id: 'lastName',cell: info => info.getValue(),header: () => <span>Last Name</span>,footer: props => props.column.id},{accessorKey: 'age',header: () => 'Age',footer: props => props.column.id},{accessorKey: 'visits',header: () => <span>Visits</span>,footer: props => props.column.id},{accessorKey: 'progress',header: 'Profile Progress',footer: props => props.column.id}],[]);const table = useReactTable({data,columns,getCoreRowModel: getCoreRowModel(),getPaginationRowModel: getPaginationRowModel(),debugTable: true});return (<><TableWrapper><Table><TableHead>{table.getHeaderGroups().map(headerGroup => (<TableHeadRow key={headerGroup.id}>{headerGroup.headers.map(header => {return (<TableHeading key={header.id} colSpan={header.colSpan} onClick={header.column.getToggleSortingHandler()} isSorted={header.column.getIsSorted()}>{header.isPlaceholder ? null : <span>{flexRender(header.column.columnDef.header, header.getContext())}</span>}</TableHeading>);})}</TableHeadRow>))}</TableHead><TableBody>{table.getRowModel().rows.map(row => {return (<TableRow key={row.id}>{row.getVisibleCells().map(cell => {return <TableData key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableData>;})}</TableRow>);})}</TableBody></Table><PaginationcanPreviousPage={table.getCanPreviousPage()}canNextPage={table.getCanNextPage()}pageOptions={table.getPageOptions()}pageCount={table.getPageCount()}gotoPage={table.setPageIndex}nextPage={table.nextPage}previousPage={table.previousPage}setPageSize={table.setPageSize}pageIndex={table.getState().pagination.pageIndex}pageSize={table.getState().pagination.pageSize}/></TableWrapper></>);};
Filter
react-table
provides mechanisms for filtering the entire data set passed to the table, not just what is displayed on the current page (particularly where pagination is in use). The below example shows how to hook up this global filtering functionality with Mesh's Textbox
(and its FormControl
wrapper).
You will also likely want to create your own fuzzy filter and debounced input. The below example shows how to set this up using the @tanstack/match-sorter-utils
package.
For more examples and detailed documentation:
- React Table Filtering API Docs
- React Table Filtering Examples
- Filtering example in storybook, opens in a new tab
First Name | Last Name | Full Name | Age | Visits | Status | Profile Progress |
---|---|---|---|---|---|---|
Kaley | Pollich | Kaley Pollich | 12 | 563 | single | 58 |
Lurline | Kerluke | Lurline Kerluke | 20 | 877 | complicated | 14 |
Raquel | Kuhic | Raquel Kuhic | 15 | 939 | single | 91 |
Vivienne | Schuppe | Vivienne Schuppe | 34 | 807 | relationship | 49 |
Isabella | Feeney | Isabella Feeney | 10 | 979 | relationship | 31 |
Harold | Keebler | Harold Keebler | 33 | 500 | complicated | 93 |
Kali | McClure | Kali McClure | 17 | 164 | complicated | 77 |
Nichole | Daugherty | Nichole Daugherty | 40 | 495 | complicated | 82 |
Dixie | Bayer | Dixie Bayer | 14 | 478 | complicated | 34 |
Bruce | Macejkovic | Bruce Macejkovic | 34 | 43 | relationship | 99 |
Pietro | Koss | Pietro Koss | 6 | 624 | single | 60 |
Karen | Torphy | Karen Torphy | 18 | 21 | single | 51 |
Marco | Abernathy | Marco Abernathy | 11 | 325 | complicated | 80 |
Lelah | Bradtke | Lelah Bradtke | 16 | 589 | complicated | 38 |
Meda | Thiel | Meda Thiel | 22 | 767 | relationship | 58 |
Elda | Berge | Elda Berge | 7 | 639 | complicated | 3 |
Lucy | Block | Lucy Block | 30 | 970 | relationship | 98 |
Adan | Mante | Adan Mante | 12 | 326 | single | 77 |
Patricia | Friesen | Patricia Friesen | 11 | 954 | relationship | 75 |
Katheryn | Walter | Katheryn Walter | 11 | 558 | relationship | 33 |
import React from 'react';import styled from 'styled-components';import {ColumnDef, flexRender, getCoreRowModel, useReactTable, getFilteredRowModel, getFacetedRowModel, getFacetedUniqueValues, getFacetedMinMaxValues, FilterFn} from '@tanstack/react-table';import {rankItem} from '@tanstack/match-sorter-utils';import {TableWrapper, Table, TableHead, TableHeadRow, TableHeading, TableBody, TableRow, TableData, SearchBox} from '@nib/table';import {SearchSystemIcon} from '@nib/icons';import {Box} from '@nib/layout';type Person = {firstName: string;lastName: string;age: number;visits: number;progress: number;subRows?: Person[];};const data = [{firstName: 'Kaley',lastName: 'Pollich',age: 12,visits: 563,progress: 58},{firstName: 'Lurline',lastName: 'Kerluke',age: 20,visits: 877,progress: 14,status: 'complicated'},{firstName: 'Raquel',lastName: 'Kuhic',age: 15,visits: 939,progress: 91},{firstName: 'Vivienne',lastName: 'Schuppe',age: 34,visits: 807,progress: 49},{firstName: 'Isabella',lastName: 'Feeney',age: 10,visits: 979,progress: 31},{firstName: 'Harold',lastName: 'Keebler',age: 33,visits: 500,progress: 93},{firstName: 'Kali',lastName: 'McClure',age: 17,visits: 164,progress: 77},{firstName: 'Theresia',lastName: 'Stroman',age: 26,visits: 614,progress: 61}];export const FilterTable = () => {const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {// Rank the itemconst itemRank = rankItem(row.getValue(columnId), value);// Store the itemRank infoaddMeta({itemRank});// Return if the item should be filtered in/outreturn itemRank.passed;};const [globalFilter, setGlobalFilter] = React.useState('');const columns = React.useMemo<ColumnDef<Person, any>[]>(() => [{accessorKey: 'firstName',enableColumnFilter: false,cell: info => info.getValue(),footer: props => props.column.id,header: () => <span>First Name</span>},{accessorFn: row => row.lastName,id: 'lastName',enableColumnFilter: false,cell: info => info.getValue(),header: () => <span>Last Name</span>,footer: props => props.column.id},{accessorFn: row => `${row.firstName} ${row.lastName}`,id: 'fullName',header: 'Full Name',enableColumnFilter: false,cell: info => info.getValue(),footer: props => props.column.id},{accessorKey: 'age',header: () => 'Age',enableColumnFilter: false,footer: props => props.column.id},{accessorKey: 'visits',enableColumnFilter: false,header: () => <span>Visits</span>,footer: props => props.column.id},{accessorKey: 'status',header: 'Status',enableColumnFilter: false,footer: props => props.column.id},{accessorKey: 'progress',header: 'Profile Progress',enableColumnFilter: false,footer: props => props.column.id}],[]);const table = useReactTable({data,columns,filterFns: {fuzzy: fuzzyFilter},state: {globalFilter},onGlobalFilterChange: setGlobalFilter,globalFilterFn: fuzzyFilter,getCoreRowModel: getCoreRowModel(),getFilteredRowModel: getFilteredRowModel(),getFacetedRowModel: getFacetedRowModel(),getFacetedUniqueValues: getFacetedUniqueValues(),getFacetedMinMaxValues: getFacetedMinMaxValues(),debugTable: true,debugHeaders: true,debugColumns: false});return (<div><div><DebouncedInput value={globalFilter ?? ''} onChange={value => setGlobalFilter(String(value))} placeholder="Search all columns..." /></div><TableWrapper><Table><TableHead>{table.getHeaderGroups().map(headerGroup => (<TableHeadRow key={headerGroup.id}>{headerGroup.headers.map(header => {return (<TableHeading key={header.id} colSpan={header.colSpan} onClick={header.column.getToggleSortingHandler()} isSorted={header.column.getIsSorted()}>{header.isPlaceholder ? null : <span>{flexRender(header.column.columnDef.header, header.getContext())}</span>}</TableHeading>);})}</TableHeadRow>))}</TableHead><TableBody stripedRows>{table.getRowModel().rows.map(row => {return (<TableRow key={row.id}>{row.getVisibleCells().map(cell => {return <TableData key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableData>;})}</TableRow>);})}</TableBody></Table></TableWrapper></div>);};// Anm example debounced input react componentfunction DebouncedInput({value: initialValue,onChange,debounce = 500,...props}: {value: string | number;onChange: (value: string | number) => void;debounce?: number;} & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'>) {const [value, setValue] = React.useState(initialValue);React.useEffect(() => {setValue(initialValue);}, [initialValue]);React.useEffect(() => {const timeout = setTimeout(() => {onChange(value);}, debounce);return () => clearTimeout(timeout);}, [value, onChange, debounce]);return (<SearchBox value={value || ''} onChange={(e: any) => setValue(e.target.value)} placeholder="Filter table" {...props}/>);}
Expanding Row
Expanding rows can be used to reveal additional information or actions about a table row. At present, a chevron at the end of the row functions as the only clickable trigger.
For more examples and detailed documentation:
- React Table Expanding API Docs
- React Table Expanding Examples
- Expanding example in storybook, opens in a new tab
First Name | Last Name | Age | Visits | Profile Progress | Created At | |
---|---|---|---|---|---|---|
Kaley | Pollich | 12 | 563 | 58 | 2001-10-22T09:20:21.683Z | |
Lurline | Kerluke | 20 | 877 | 14 | 1995-09-28T06:49:17.599Z | |
Raquel | Kuhic | 15 | 939 | 91 | 1993-12-12T02:42:59.627Z | |
Vivienne | Schuppe | 34 | 807 | 49 | 1991-02-08T13:16:13.271Z | |
Isabella | Feeney | 10 | 979 | 31 | 2013-09-07T06:27:50.422Z | |
Harold | Keebler | 33 | 500 | 93 | 1994-11-01T21:55:06.822Z | |
Kali | McClure | 17 | 164 | 77 | 1999-04-27T05:42:43.256Z | |
Nichole | Daugherty | 40 | 495 | 82 | 2013-04-01T19:24:32.137Z | |
Dixie | Bayer | 14 | 478 | 34 | 2012-09-08T18:07:26.336Z | |
Bruce | Macejkovic | 34 | 43 | 99 | 2013-07-08T21:34:36.296Z |
import React from 'react';import {ColumnDef, flexRender, getCoreRowModel, getSortedRowModel, SortingState, useReactTable, getPaginationRowModel, getExpandedRowModel, ExpandedState, Row} from '@tanstack/react-table';import {TableWrapper, Table, TableHead, TableHeadRow, TableHeading, TableBody, TableExpandRow, TableData, TableExpandHeading} from '@nib/table';type Person = {firstName: string;lastName: string;age: number;visits: number;progress: number;subRows?: Person[];};const data = [{firstName: 'Kaley',lastName: 'Pollich',age: 12,visits: 563,progress: 58},{firstName: 'Lurline',lastName: 'Kerluke',age: 20,visits: 877,progress: 14,status: 'complicated'},{firstName: 'Raquel',lastName: 'Kuhic',age: 15,visits: 939,progress: 91},{firstName: 'Vivienne',lastName: 'Schuppe',age: 34,visits: 807,progress: 49,status: 'relationship'},{firstName: 'Isabella',lastName: 'Feeney',age: 10,visits: 979,progress: 31,status: 'relationship'},{firstName: 'Harold',lastName: 'Keebler',age: 33,visits: 500,progress: 93,status: 'complicated'},{firstName: 'Kali',lastName: 'McClure',age: 17,visits: 164,progress: 77,status: 'complicated'},{firstName: 'Theresia',lastName: 'Stroman',age: 26,visits: 614,progress: 61,status: 'complicated'}];export const ExpandingRowTable = () => {const columns = React.useMemo<ColumnDef<Person>[]>(() => [{accessorKey: 'firstName',header: () => <>First Name</>,cell: ({getValue}) => getValue(),footer: props => props.column.id},{accessorFn: row => row.lastName,id: 'lastName',cell: info => info.getValue(),header: () => <span>Last Name</span>,footer: props => props.column.id},{accessorKey: 'age',header: () => 'Age',footer: props => props.column.id},{accessorKey: 'visits',header: () => <span>Visits</span>,footer: props => props.column.id},{accessorKey: 'progress',header: 'Profile Progress',footer: props => props.column.id}],[]);const [expanded, setExpanded] = React.useState < ExpandedState > {};const table = useReactTable({data,columns,state: {expanded},onExpandedChange: setExpanded,getCoreRowModel: getCoreRowModel(),getExpandedRowModel: getExpandedRowModel(),getRowCanExpand: () => true,debugTable: true});return (<><TableWrapper><Table><TableHead>{table.getHeaderGroups().map(headerGroup => (<TableHeadRow key={headerGroup.id}>{headerGroup.headers.map(header => {return (<TableHeading key={header.id} colSpan={header.colSpan} onClick={header.column.getToggleSortingHandler()} isSorted={header.column.getIsSorted()}>{header.isPlaceholder ? null : <span>{flexRender(header.column.columnDef.header, header.getContext())}</span>}</TableHeading>);})}<TableExpandHeading /> {/* Empty th for expander column */}</TableHeadRow>))}</TableHead><TableBody stripedRows>{table.getRowModel().rows.map(row => {return (<><TableExpandRow key={row.id} isExpanded={row.getIsExpanded()} onClick={row.getToggleExpandedHandler()}>{row.getVisibleCells().map(cell => {return <TableData key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableData>;})}</TableExpandRow>{row.getIsExpanded() && (<tr>{/* 2nd row is a custom 1 cell row (+1 to cater for empty TableExpandHeading) */}<td colSpan={row.getVisibleCells().length + 1}>{renderSubComponent({row})}</td></tr>)}</>);})}</TableBody></Table></TableWrapper></>);};const renderSubComponent = ({row}: {row: Row<Person>}) => {return <p>Render extra details here about {row.original.firstName}</p>;};
Props
As we have built this package on react-table, we rely on their implementation of the columns
and data
props to populate the table.
Also, as the package adopts the "headless" approach and you must provide the markup for your table, the below props must be set on the appropriate table components. The Target Column indicates which component(s) accept the prop.
Prop | Type | Default | Target | Description |
---|---|---|---|---|
rowHeight | string | 'regular' | TableHeadRow , TableRow | The height of the table rows. Must be one of 'relaxed' , 'regular' , 'condensed' . |
stripedRows | boolean | false | TableBody | Applies alternate colours to the table rows for improved scanability. |
equalColumns | boolean | false | Table | Sets an equal width to all columns. |
fixedHeader | boolean | false | TableHeadRow | Fixes the header to the top of the table when vertically scrolling. |
fixedColumn | boolean | false | TableHeading , TableData | Fixes the first column to the left of the table when horizontally scrolling. |
rowHover | boolean | false | TableBody | Enables a hover state when the cursor moves over a row. This may assist a user in scanning rows, but can imply interactivity where there is none. |
Considerations
Columns
Columns plays a key role in defining the datatable. Most of the functionality depends on the column defs types and Column Helpers.
For further reading and documentation:
- https://tanstack.com/table/v8/docs/api/core/column
- https://tanstack.com/table/v8/docs/api/core/column-def
Ensure content is organised and intuitive
Structured tables should organize content in a meaningful way, such as using hierarchy or alphabetisation, and follow a logical structure that makes content easy to understand.
Consider typography and alignment
Headers should be set in Title case, while all other text is set in sentence case. All typography is left-aligned by default. This helps make the data easily scannable, readable and comparable. The one exception is numeric data, which should be right aligned to help users quickly identify larger numbers.
Be efficient with your content
Using concise, scannable and objective language increases usabilty and readability. Consider using only 45 to 75 characters (including spaces and punctuation) per line.
Utilise visual cues
Using differently-coloured backgrounds can give organisational context and meaning to your table. Whether for your header or for alternating rows, these visual cues can help present data in a way that is easier to scan and understand.
The stripedRows
prop can help achieve this and is recommended for larger data sets, where the alternated pattern can improve a users's speed of comprehension when reading along a row.
Consider column widths
When presenting data that is similar or comparable between columns, consider using even column widths. This can be achieved by using the equalColumns
prop.
At times, content might need to be structured to fit disproportionately, allowing for flexibility of headers and corresponding columns to be changed based on content length. The number of characters for readability per line should not go beyond 45 to 75 characters (including spaces and punctuation).
In tables that use uneven column widths, ensure that the collapseColumn
prop is not applied to the last column in the table.
Use row height props effectively
When choosing row heights, be sure to consider the type and volume of data in your table. regular
and relaxed
row heights offer more white space, and are more comfortable when working with large datasets. Using a condensed
row height will allow the user to view more data at once without having to scroll, but will reduce the table's readability and potentially cause parsing errors for the user.
In summary:
- Use
relaxed
row heights when you have visually-heavy content or a dataset of less than 25 rows. - Use
regular
row heights (default) when you only have a couple of words per column and need to provide easy scanability between . - Use
condensed
row heights when space is limited or for more numerical datasets.
Maintain context while scrolling
Anchoring contextual information will help users understand what data they're looking at, particularly when scrolling down or across a table. This functionality is important when designing tables with large datasets or on smaller screens.
The two props that will assist in this design are fixedHeader
and fixedColumn
.
Consider responsive behaviour
Along with maintaining context while scrolling, the number of columns that fit on a mobile screen without scrolling is important to consider. Items need to be legible without requiring the user to zoom in. For complex or wordy entries, such as those in comparison tables, only 2 columns may fit legibly on a narrow mobile screen. For a number-heavy table, narrower columns may work, allowing more columns to be visible.
To make certain column widths smaller, consider using the collapse
option in the columns
prop array.
More information
Below links provide a complete guidance and how to use the functionalities which were not listed in the above section.