Layout

Table

A table displays large quantities of items or data in a list format. Filtering features, date picker, collapsible rows and actions may be added.
1
import {
2
BlankSlate,
3
Box,
4
Button,
5
ColumnDef,
6
createColumnHelper,
7
DateRangePickerPreset,
8
onTableChangeEvent,
9
Table,
10
Title,
11
useTable,
12
} from '@coveord/plasma-mantine';
13
import {EditSize16Px} from '@coveord/plasma-react-icons';
14
import dayjs from 'dayjs';
15
import {FunctionComponent, useState} from 'react';
16
17
interface IExampleRowData {
18
userId: number;
19
id: number;
20
title: string;
21
body: string;
22
}
23
24
const columnHelper = createColumnHelper<IExampleRowData>();
25
26
/**
27
* Define your columns outside the component rendering the table
28
* (or memoize them) to avoid unnecessary render loops
29
*/
30
const columns: Array<ColumnDef<IExampleRowData>> = [
31
columnHelper.accessor('userId', {
32
header: 'User ID',
33
cell: (info) => info.row.original.userId,
34
}),
35
columnHelper.accessor('id', {
36
header: 'Post ID',
37
cell: (info) => info.row.original.id,
38
}),
39
columnHelper.accessor('title', {
40
header: 'Title',
41
cell: (info) => info.row.original.title,
42
}),
43
Table.CollapsibleColumn as ColumnDef<IExampleRowData>,
44
// or if you prefer an accordion behaviour
45
// Table.AccordionColumn as ColumnDef<IExampleRowData>,
46
];
47
48
const Demo = () => {
49
const [data, setData] = useState(null);
50
const [loading, setLoading] = useState(true);
51
const [pages, setPages] = useState(1);
52
53
const fetchData: onTableChangeEvent<IExampleRowData> = async (state) => {
54
setLoading(true);
55
const searchParams = new URLSearchParams({
56
_sort: state.sorting?.[0]?.id ?? 'userId',
57
_order: state.sorting?.[0]?.desc ? 'desc' : 'asc',
58
_page: (state.pagination.pageIndex + 1).toString(),
59
_limit: state.pagination.pageSize.toString(),
60
userId: state.predicates.user,
61
title_like: state.globalFilter,
62
});
63
if (state.predicates.user === '') {
64
searchParams.delete('userId');
65
}
66
if (!state.globalFilter) {
67
searchParams.delete('title_like');
68
}
69
try {
70
const response = await fetch(`https://jsonplaceholder.typicode.com/posts?${searchParams.toString()}`);
71
const body = await response.json();
72
setData(body);
73
setPages(Math.ceil(Number(response.headers.get('x-total-count')) / state.pagination?.pageSize));
74
} catch (e) {
75
console.error(e);
76
} finally {
77
setLoading(false);
78
}
79
};
80
81
return (
82
<Table
83
data={data}
84
getRowId={({id}) => id.toString()}
85
columns={columns}
86
noDataChildren={<NoData />}
87
onMount={fetchData}
88
onChange={fetchData}
89
loading={loading}
90
initialState={{dateRange: [previousDay, today], predicates: {user: ''}}}
91
getExpandChildren={(datum) => <Box py="xs">{datum.body}</Box>}
92
>
93
{/* you can override background color with: sx={{backgroundColor: 'white'}} for Header and Footer */}
94
<Table.Header>
95
<Table.Actions>{(datum: IExampleRowData) => <TableActions datum={datum} />}</Table.Actions>
96
<UserPredicate />
97
<Table.Filter placeholder="Search posts by title" />
98
<Table.DateRangePicker
99
rangeCalendarProps={{maxDate: dayjs().endOf('day').toDate()}}
100
presets={DatePickerPresets}
101
/>
102
</Table.Header>
103
<Table.Footer>
104
<Table.PerPage />
105
<Table.Pagination totalPages={pages} />
106
</Table.Footer>
107
</Table>
108
);
109
};
110
export default Demo;
111
112
const NoData: FunctionComponent = () => {
113
const {clearFilters, isFiltered} = useTable();
114
115
return isFiltered ? (
116
<BlankSlate>
117
<Title order={4}>No data found for those filters</Title>
118
<Button onClick={clearFilters}>Clear filters</Button>
119
</BlankSlate>
120
) : (
121
<BlankSlate>
122
<Title order={4}>No Data</Title>
123
</BlankSlate>
124
);
125
};
126
127
const today: Date = dayjs().startOf('day').toDate();
128
const previousDay: Date = dayjs().subtract(1, 'day').endOf('day').toDate();
129
const previousWeek: Date = dayjs().subtract(1, 'week').endOf('day').toDate();
130
131
const DatePickerPresets: Record<string, DateRangePickerPreset> = {
132
lastDay: {label: 'Last 24 hours', range: [previousDay, today]},
133
lastWeek: {label: 'Last week', range: [previousWeek, today]},
134
};
135
136
const TableActions: FunctionComponent<{datum: IExampleRowData}> = ({datum}) => {
137
const actionCondition = datum.id % 2 === 0 ? true : false;
138
const pressedAction = () => alert('Edit action is triggered!');
139
return (
140
<>
141
{actionCondition ? (
142
<Button variant="subtle" onClick={pressedAction} leftIcon={<EditSize16Px height={16} />}>
143
Edit
144
</Button>
145
) : null}
146
</>
147
);
148
};
149
150
const UserPredicate: FunctionComponent = () => (
151
<Table.Predicate
152
id="user"
153
label="User"
154
data={[
155
{
156
value: '',
157
label: 'All',
158
},
159
{value: '1', label: '1'},
160
{value: '2', label: '2'},
161
{value: '3', label: '3'},
162
{value: '4', label: '4'},
163
{value: '5', label: '5'},
164
{value: '6', label: '6'},
165
{value: '7', label: '7'},
166
{value: '8', label: '8'},
167
{value: '9', label: '9'},
168
{value: '10', label: '10'},
169
]}
170
/>
171
);

Props

NameTypeDefaultDescription
columnsrequiredColumnDef<unknown>[]
Columns to display in the table.
datarequiredunknown[]
Data to display in the table
additionalRootNodesHTMLElement[]
Nodes that are considered inside the table. Rows normally get unselected when clicking outside the table, but sometimes it has difficulties guessing what is inside or outside, for example when using modals. You can use this prop to force the table to consider some nodes to be inside the table.
childrenReactNode
Childrens to display in the table. They need to be wrap in either `Table.Header` or `Table.Footer`
disableRowSelectionbooleanfalse
Whether row selection is enabled or not
doubleClickAction(datum: unknown) => void
Action passed when user double clicks on a row
getExpandChildren(datum: unknown) => ReactNode
Function that generates the expandable content of a row Return null for rows that don't need to be expandable
  • datumthe row for which the children should be generated.
getRowId(originalRow: unknown, index: number, parent?: Row<unknown>) => string
Defines how each row is uniquely identified. It is highly recommended that you specify this prop to an ID that makes sense.
initialStateInitialTableState<unknown>
Initial state of the table
layoutsTableLayout[][Table.Layouts.Rows]
Available layouts
loadingbooleanfalse
Whether the table is loading or not
multiRowSelectionEnabledbooleanfalse
Whether the user can select multiple rows in order to perform actions in bulk
noDataChildrenReactNode
React children to show when the table has no rows to show. You can leverage useTable to get the state of the table
onChangeonTableChangeEvent<unknown>
Function called when the table should update
  • statethe state of the table
onMountonTableChangeEvent<unknown>
Function called when the table mounts
  • statethe state of the table
onRowSelectionChange(selectedRows: unknown[]) => void
Function called whenever the row selection changes
  • selectedRowsThe selected rows
optionsOmit<Partial<TableOptions<unknown>>, "getRowId" | "data" | "initialState" | "columns" | "getRowCanExpand" | "manualPagination" | "enableRowSelection" | "enableMultiRowSelection" | "onRowSelectionChange">
Additional options that can be passed to the table

Examples

Table with bulk selection of rows
1
import {
2
BlankSlate,
3
Button,
4
ColumnDef,
5
createColumnHelper,
6
onTableChangeEvent,
7
Table,
8
Title,
9
useTable,
10
} from '@coveord/plasma-mantine';
11
import {DeleteSize16Px, EditSize16Px} from '@coveord/plasma-react-icons';
12
import {FunctionComponent, useState} from 'react';
13
14
interface IExampleRowData {
15
userId: number;
16
id: number;
17
title: string;
18
body: string;
19
}
20
21
const columnHelper = createColumnHelper<IExampleRowData>();
22
const columns: Array<ColumnDef<IExampleRowData>> = [
23
columnHelper.accessor('userId', {
24
header: 'User ID',
25
cell: (info) => info.row.original.userId,
26
enableSorting: false,
27
}),
28
columnHelper.accessor('title', {
29
header: 'Title',
30
cell: (info) => info.row.original.title,
31
enableSorting: false,
32
}),
33
];
34
35
const Demo = () => {
36
const [data, setData] = useState(null);
37
const [loading, setLoading] = useState(true);
38
const [pages, setPages] = useState(1);
39
40
const fetchData: onTableChangeEvent<IExampleRowData> = async (state) => {
41
setLoading(true);
42
const searchParams = new URLSearchParams({
43
_page: (state.pagination.pageIndex + 1).toString(),
44
_limit: state.pagination.pageSize.toString(),
45
title_like: state.globalFilter,
46
});
47
if (!state.globalFilter) {
48
searchParams.delete('title_like');
49
}
50
try {
51
const response = await fetch(`https://jsonplaceholder.typicode.com/posts?${searchParams.toString()}`);
52
const body = await response.json();
53
setData(body);
54
setPages(Math.ceil(Number(response.headers.get('x-total-count')) / state.pagination?.pageSize));
55
} catch (e) {
56
console.error(e);
57
} finally {
58
setLoading(false);
59
}
60
};
61
62
return (
63
<Table<IExampleRowData>
64
data={data}
65
getRowId={({id}) => id.toString()}
66
columns={columns}
67
noDataChildren={<NoData />}
68
onMount={fetchData}
69
onChange={fetchData}
70
loading={loading}
71
onRowSelectionChange={(selectedRows) =>
72
console.info(`Row selection changed, selected rows: ${selectedRows.map(({id}) => id).join(', ')}`)
73
}
74
multiRowSelectionEnabled
75
initialState={{
76
rowSelection: {
77
'1': {
78
userId: 1,
79
id: 1,
80
title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit',
81
body: 'quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto',
82
},
83
'2': {
84
userId: 1,
85
id: 2,
86
title: 'qui est esse',
87
body: 'est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla',
88
},
89
},
90
}}
91
>
92
<Table.Header>
93
<Table.Actions>
94
{(selectedRows: IExampleRowData[]) => <TableActions data={selectedRows} />}
95
</Table.Actions>
96
<Table.Filter placeholder="Search posts by title" />
97
</Table.Header>
98
<Table.Footer>
99
<Table.PerPage />
100
<Table.Pagination totalPages={pages} />
101
</Table.Footer>
102
</Table>
103
);
104
};
105
export default Demo;
106
107
const NoData: FunctionComponent = () => {
108
const {isFiltered, clearFilters} = useTable();
109
110
return isFiltered ? (
111
<BlankSlate>
112
<Title order={4}>No data found for those filters</Title>
113
<Button onClick={clearFilters}>Clear filters</Button>
114
</BlankSlate>
115
) : (
116
<BlankSlate>
117
<Title order={4}>No Data</Title>
118
</BlankSlate>
119
);
120
};
121
122
const TableActions: FunctionComponent<{data: IExampleRowData[]}> = ({data}) => {
123
if (data.length === 1) {
124
return (
125
<Button
126
variant="subtle"
127
onClick={() => alert(`Action triggered on a single row: ${data[0].id}`)}
128
leftIcon={<EditSize16Px height={16} />}
129
>
130
Single row action
131
</Button>
132
);
133
}
134
if (data.length > 1) {
135
return (
136
<Button
137
variant="subtle"
138
onClick={() => alert(`Bulk action triggered on multiple rows: ${data.map(({id}) => id).join(', ')}`)}
139
leftIcon={<DeleteSize16Px height={16} />}
140
>
141
Bulk action
142
</Button>
143
);
144
}
145
146
return null;
147
};
Table with disabled row selection
1
import {
2
BlankSlate,
3
Button,
4
ColumnDef,
5
createColumnHelper,
6
onTableChangeEvent,
7
Table,
8
Title,
9
useTable,
10
} from '@coveord/plasma-mantine';
11
import {FunctionComponent, useState} from 'react';
12
13
interface IExampleRowData {
14
userId: number;
15
id: number;
16
title: string;
17
body: string;
18
}
19
20
const columnHelper = createColumnHelper<IExampleRowData>();
21
const columns: Array<ColumnDef<IExampleRowData>> = [
22
columnHelper.accessor('userId', {
23
header: 'User ID',
24
cell: (info) => info.row.original.userId,
25
enableSorting: false,
26
}),
27
columnHelper.accessor('title', {
28
header: 'Title',
29
cell: (info) => info.row.original.title,
30
enableSorting: false,
31
}),
32
];
33
34
const Demo = () => {
35
const [data, setData] = useState(null);
36
const [loading, setLoading] = useState(true);
37
const [pages, setPages] = useState(1);
38
39
const fetchData: onTableChangeEvent<IExampleRowData> = async (state) => {
40
setLoading(true);
41
const searchParams = new URLSearchParams({
42
_page: (state.pagination.pageIndex + 1).toString(),
43
_limit: state.pagination.pageSize.toString(),
44
title_like: state.globalFilter,
45
});
46
if (!state.globalFilter) {
47
searchParams.delete('title_like');
48
}
49
try {
50
const response = await fetch(`https://jsonplaceholder.typicode.com/posts?${searchParams.toString()}`);
51
const body = await response.json();
52
setData(body);
53
setPages(Math.ceil(Number(response.headers.get('x-total-count')) / state.pagination?.pageSize));
54
} catch (e) {
55
console.error(e);
56
} finally {
57
setLoading(false);
58
}
59
};
60
61
return (
62
<Table<IExampleRowData>
63
data={data}
64
getRowId={({id}) => id.toString()}
65
columns={columns}
66
noDataChildren={<NoData />}
67
onMount={fetchData}
68
onChange={fetchData}
69
loading={loading}
70
onRowSelectionChange={(selectedRows) =>
71
console.info(`Row selection changed, selected rows: ${selectedRows.map(({id}) => id).join(', ')}`)
72
}
73
multiRowSelectionEnabled
74
disableRowSelection
75
initialState={{
76
rowSelection: {
77
'1': {
78
userId: 1,
79
id: 1,
80
title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit',
81
body: 'quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto',
82
},
83
'2': {
84
userId: 1,
85
id: 2,
86
title: 'qui est esse',
87
body: 'est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla',
88
},
89
},
90
}}
91
>
92
<Table.Header>
93
<Table.Filter placeholder="Search posts by title" />
94
</Table.Header>
95
<Table.Footer>
96
<Table.PerPage />
97
<Table.Pagination totalPages={pages} />
98
</Table.Footer>
99
</Table>
100
);
101
};
102
export default Demo;
103
104
const NoData: FunctionComponent = () => {
105
const {isFiltered, clearFilters} = useTable();
106
107
return isFiltered ? (
108
<BlankSlate>
109
<Title order={4}>No data found for those filters</Title>
110
<Button onClick={clearFilters}>Clear filters</Button>
111
</BlankSlate>
112
) : (
113
<BlankSlate>
114
<Title order={4}>No Data</Title>
115
</BlankSlate>
116
);
117
};
Table with client side pagination, sorting, and filtering
Moshe
Abbott
10
Talia
Klocko
17
Savion
Schmidt
2
Jamir
Gulgowski
3
Neal
Beier
13
Results per page
1
import {
2
ColumnDef,
3
createColumnHelper,
4
FilterFn,
5
getFilteredRowModel,
6
getPaginationRowModel,
7
getSortedRowModel,
8
Table,
9
TableProps,
10
} from '@coveord/plasma-mantine';
11
import {faker} from '@faker-js/faker';
12
import {rankItem} from '@tanstack/match-sorter-utils';
13
import {useMemo} from 'react';
14
15
export type Person = {
16
id: string;
17
firstName: string;
18
lastName: string;
19
age: number;
20
};
21
22
const makeData = (len: number): Person[] =>
23
Array(len)
24
.fill(0)
25
.map(() => ({
26
id: faker.datatype.uuid(),
27
firstName: faker.name.firstName(),
28
lastName: faker.name.lastName(),
29
age: faker.datatype.number(40),
30
}));
31
32
const fuzzyFilter: FilterFn<Person> = (row, columnId, value) => rankItem(row.getValue(columnId), value).passed;
33
34
const columnHelper = createColumnHelper<Person>();
35
36
const columns: Array<ColumnDef<Person>> = [
37
columnHelper.accessor('firstName', {
38
header: 'First name',
39
cell: (info) => info.row.original.firstName,
40
}),
41
columnHelper.accessor('lastName', {
42
header: 'Last name',
43
cell: (info) => info.row.original.lastName,
44
}),
45
columnHelper.accessor('age', {
46
header: 'Age',
47
cell: (info) => info.row.original.age,
48
}),
49
];
50
51
const options: TableProps<Person>['options'] = {
52
globalFilterFn: fuzzyFilter,
53
getFilteredRowModel: getFilteredRowModel(),
54
getPaginationRowModel: getPaginationRowModel(),
55
getSortedRowModel: getSortedRowModel(),
56
};
57
58
const Demo = () => {
59
const data = useMemo(() => makeData(45), []);
60
return (
61
<Table
62
data={data}
63
columns={columns}
64
options={options}
65
initialState={{pagination: {pageSize: 5}}}
66
getRowId={({id}) => id}
67
>
68
<Table.Header>
69
<Table.Filter placeholder="Search" />
70
</Table.Header>
71
<Table.Footer>
72
<Table.PerPage values={[5, 10, 25]} />
73
<Table.Pagination totalPages={null} />
74
</Table.Footer>
75
</Table>
76
);
77
};
78
export default Demo;
Table with empty states

No data found for filter "foo"

1
import {BlankSlate, Button, ColumnDef, createColumnHelper, Table, Title, useTable} from '@coveord/plasma-mantine';
2
import {NoContentSize32Px} from '@coveord/plasma-react-icons';
3
4
export type Person = {
5
firstName: string;
6
lastName: string;
7
age: number;
8
};
9
10
const NoData = () => {
11
const {isFiltered, clearFilters, state} = useTable();
12
13
return isFiltered ? (
14
<BlankSlate>
15
<Title order={4}>No data found for filter "{state.globalFilter}"</Title>
16
<Button onClick={clearFilters}>Clear filter</Button>
17
</BlankSlate>
18
) : (
19
<BlankSlate withBorder={false}>
20
<NoContentSize32Px height={64} />
21
<Title order={4}>Hello Empty State</Title>
22
</BlankSlate>
23
);
24
};
25
26
const Demo = () => (
27
<Table
28
data={[]}
29
columns={columns}
30
noDataChildren={<NoData />}
31
initialState={{globalFilter: 'foo', pagination: {pageSize: 5}}}
32
>
33
<Table.Header>
34
<Table.Filter placeholder="Search" />
35
</Table.Header>
36
<Table.Footer>
37
<Table.PerPage values={[5, 10, 25]} />
38
<Table.Pagination totalPages={null} />
39
</Table.Footer>
40
</Table>
41
);
42
export default Demo;
43
44
const columnHelper = createColumnHelper<Person>();
45
46
const columns: Array<ColumnDef<Person>> = [
47
columnHelper.accessor('firstName', {
48
header: 'First name',
49
cell: (info) => info.row.original.firstName,
50
}),
51
columnHelper.accessor('lastName', {
52
header: 'Last name',
53
cell: (info) => info.row.original.lastName,
54
}),
55
columnHelper.accessor('age', {
56
header: 'Age',
57
cell: (info) => info.row.original.age,
58
}),
59
];
Table with a child component using the hook to re-fetch
1
import {ColumnDef, createColumnHelper, onTableChangeEvent, Table, useTable} from '@coveord/plasma-mantine';
2
import {FunctionComponent, useEffect, useState} from 'react';
3
4
interface PostData {
5
id: number;
6
title: string;
7
}
8
9
const columnHelper = createColumnHelper<PostData>();
10
const columns: Array<ColumnDef<PostData>> = [
11
columnHelper.accessor('id', {
12
header: 'Post ID',
13
enableSorting: false,
14
}),
15
columnHelper.accessor('title', {
16
header: 'Title',
17
enableSorting: false,
18
}),
19
];
20
21
const Demo = () => {
22
const [data, setData] = useState(null);
23
const [loading, setLoading] = useState(true);
24
25
const fetchData: onTableChangeEvent<PostData> = async () => {
26
setLoading(true);
27
try {
28
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
29
const body = await response.json();
30
31
// slow down the fetch, to make the refresh more obvious
32
await new Promise((resolve) => setTimeout(resolve, 1000));
33
34
setData(body);
35
} catch (e) {
36
console.error(e);
37
} finally {
38
setLoading(false);
39
}
40
};
41
42
return (
43
<Table<PostData>
44
data={data}
45
getRowId={({id}) => id.toString()}
46
columns={columns}
47
onMount={fetchData}
48
onChange={fetchData}
49
loading={loading}
50
>
51
<Table.Consumer>
52
{/* Refresh the component every 10s, look at your network tab to validate it works */}
53
<IntervalRefresh every={10 * 1000} />
54
</Table.Consumer>
55
<Table.LastUpdated />
56
</Table>
57
);
58
};
59
export default Demo;
60
61
const IntervalRefresh: FunctionComponent<{every: number}> = ({every}) => {
62
const {onChange} = useTable();
63
useEffect(() => {
64
const timer = setInterval(() => onChange(), every);
65
return () => clearInterval(timer);
66
}, [every]);
67
68
return null;
69
};

No guidelines exist for Table yet.

Create guidelines