Skip to content
Experimental

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

bash
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:

ComponentDescription
TableWrapperoptional outer wrapper to control horizontal overflow
TableCaptionoptional caption above or below the table
Tablethe table element
TableHeadthe thead element
TableHeadRowthe tr element for use within the TableHead
TableHeadinga th element, with prebuilt functionality to support sorting
Tha static th element
TableBodythe tbody element
TableRowthe tr element
TableDatathe td element
TableExpandHeadingan empty th element for use in expanding tables
TableExpandRowthe tr element for use in expanding rows
TableExpandDatathe td element containing the button and chevron for expanding tables
IndeterminateCheckboxa checkbox for use with a row selecting table
PaginationA collection of pagination controls
SearchBoxA 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:

jsx
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.

column1column2column3column4column5column6
Column 1Column 2Column 3Column 4Column 5Column 6
Column 1Column 2Column 3Column 4Column 5Column 6
Lorem Ipsum is simply dummy text Column 2Column 3Column 4Column 5Column 6
Column 1Column 2Column 3Column 4Column 5Column 6
Column 1Column 2Column 3Column 4Column 5Column 6
jsx
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:

KaleyPollich12563single582001-10-22T09:20:21.683Z
LurlineKerluke20877complicated141995-09-28T06:49:17.599Z
RaquelKuhic15939single911993-12-12T02:42:59.627Z
VivienneSchuppe34807relationship491991-02-08T13:16:13.271Z
IsabellaFeeney10979relationship312013-09-07T06:27:50.422Z
HaroldKeebler33500complicated931994-11-01T21:55:06.822Z
KaliMcClure17164complicated771999-04-27T05:42:43.256Z
NicholeDaugherty40495complicated822013-04-01T19:24:32.137Z
DixieBayer14478complicated342012-09-08T18:07:26.336Z
BruceMacejkovic3443relationship992013-07-08T21:34:36.296Z
jsx
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 (
<TableHeading
key={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:

First NameLast NameAgeVisitsStatusProfile Progress
KaleyPollich12563single58
LurlineKerluke20877complicated14
RaquelKuhic15939single91
VivienneSchuppe34807relationship49
IsabellaFeeney10979relationship31
HaroldKeebler33500complicated93
KaliMcClure17164complicated77
NicholeDaugherty40495complicated82
DixieBayer14478complicated34
BruceMacejkovic3443relationship99
jsx
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';
import shortid from 'shortid';
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 columns = React.useMemo<ColumnDef<Person>[]>(
() => [
{
id: 'select',
header: ({table}) => (
<IndeterminateCheckbox
{...{
id: shortid.generate(),
checked: table.getIsAllRowsSelected(),
indeterminate: table.getIsSomeRowsSelected(),
onChange: table.getToggleAllRowsSelectedHandler()
}}
/>
),
cell: ({row}) => (
<div>
<IndeterminateCheckbox
{...{
id: `row-${row.id}-${shortid.generate()}`,
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.

jsx

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:

First NameLast NameAgeVisitsStatusProfile ProgressCreated At
KaleyPollich12563single582001-10-22T09:20:21.683Z
LurlineKerluke20877complicated141995-09-28T06:49:17.599Z
RaquelKuhic15939single911993-12-12T02:42:59.627Z
VivienneSchuppe34807relationship491991-02-08T13:16:13.271Z
IsabellaFeeney10979relationship312013-09-07T06:27:50.422Z
HaroldKeebler33500complicated931994-11-01T21:55:06.822Z
KaliMcClure17164complicated771999-04-27T05:42:43.256Z
NicholeDaugherty40495complicated822013-04-01T19:24:32.137Z
DixieBayer14478complicated342012-09-08T18:07:26.336Z
BruceMacejkovic3443relationship992013-07-08T21:34:36.296Z
Page 1 of 3
Go to page:
jsx
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>
<Pagination
canPreviousPage={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:

First NameLast NameFull NameAgeVisitsStatusProfile Progress
KaleyPollichKaley Pollich12563single58
LurlineKerlukeLurline Kerluke20877complicated14
RaquelKuhicRaquel Kuhic15939single91
VivienneSchuppeVivienne Schuppe34807relationship49
IsabellaFeeneyIsabella Feeney10979relationship31
HaroldKeeblerHarold Keebler33500complicated93
KaliMcClureKali McClure17164complicated77
NicholeDaughertyNichole Daugherty40495complicated82
DixieBayerDixie Bayer14478complicated34
BruceMacejkovicBruce Macejkovic3443relationship99
PietroKossPietro Koss6624single60
KarenTorphyKaren Torphy1821single51
MarcoAbernathyMarco Abernathy11325complicated80
LelahBradtkeLelah Bradtke16589complicated38
MedaThielMeda Thiel22767relationship58
EldaBergeElda Berge7639complicated3
LucyBlockLucy Block30970relationship98
AdanManteAdan Mante12326single77
PatriciaFriesenPatricia Friesen11954relationship75
KatherynWalterKatheryn Walter11558relationship33
jsx
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 item
const itemRank = rankItem(row.getValue(columnId), value);
// Store the itemRank info
addMeta({
itemRank
});
// Return if the item should be filtered in/out
return 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 component
function 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:

First NameLast NameAgeVisitsProfile ProgressCreated At
KaleyPollich12563582001-10-22T09:20:21.683Z
LurlineKerluke20877141995-09-28T06:49:17.599Z
RaquelKuhic15939911993-12-12T02:42:59.627Z
VivienneSchuppe34807491991-02-08T13:16:13.271Z
IsabellaFeeney10979312013-09-07T06:27:50.422Z
HaroldKeebler33500931994-11-01T21:55:06.822Z
KaliMcClure17164771999-04-27T05:42:43.256Z
NicholeDaugherty40495822013-04-01T19:24:32.137Z
DixieBayer14478342012-09-08T18:07:26.336Z
BruceMacejkovic3443992013-07-08T21:34:36.296Z
jsx
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.

PropTypeDefaultTargetDescription
rowHeightstring'regular'TableHeadRow, TableRowThe height of the table rows. Must be one of 'relaxed', 'regular', 'condensed'.
stripedRowsbooleanfalseTableBodyApplies alternate colours to the table rows for improved scanability.
equalColumnsbooleanfalseTableSets an equal width to all columns.
fixedHeaderbooleanfalseTableHeadRowFixes the header to the top of the table when vertically scrolling.
fixedColumnbooleanfalseTableHeading, TableDataFixes the first column to the left of the table when horizontally scrolling.
rowHoverbooleanfalseTableBodyEnables 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:

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.