MRT logoMantine React Table

Advanced Example

Here is a more advanced example showcasing Mantine React Table's many features. Features such as row selection, expanding detail panels, header groups, column ordering, column pinning, column grouping, custom column and cell renders, etc., can be seen here.
This example is still only using client-side features. If you want to see an example of how to use Mantine React Table with server side logic and remote data, check out either the Remote Data Example or the React-Query Example.
avatarJoseph Hand
$57,752
Customer Directives Architect12/28/2022
avatarPaula Kohler
$47,029
Direct Configuration Agent1/4/2023
avatarDomenic Cassin
$55,602
Corporate Operations Planner2/7/2022
avatarRey Runte
$88,782
Direct Optimization Manager10/2/2022
avatarBuck Mosciski
$95,101
Internal Mobility Orchestrator11/14/2022
avatarJohnson Nitzsche
$96,104
Lead Accounts Director8/12/2022
avatarSilas Hermiston
$64,532
International Operations Consultant6/3/2022
avatarKailey Bergstrom
$26,096
Regional Web Planner10/17/2022
avatarLilian Tromp
$72,692
Central Implementation Orchestrator9/13/2022
avatarMaxine Schmidt
$89,317
Principal Communications Orchestrator7/1/2022
1
import { useMemo } from 'react';
2
import {
3
MantineReactTable,
4
useMantineReactTable,
5
type MRT_ColumnDef,
6
MRT_GlobalFilterTextInput,
7
MRT_ToggleFiltersButton,
8
} from 'mantine-react-table';
9
import { Box, Button, Flex, Menu, Text, Title } from '@mantine/core';
10
import { IconUserCircle, IconSend } from '@tabler/icons-react';
11
import { data } from './makeData';
12
13
export type Employee = {
14
firstName: string;
15
lastName: string;
16
email: string;
17
jobTitle: string;
18
salary: number;
19
startDate: string;
20
signatureCatchPhrase: string;
21
avatar: string;
22
};
23
24
const Example = () => {
25
const columns = useMemo<MRT_ColumnDef<Employee>[]>(
26
() => [
27
{
28
id: 'employee', //id used to define `group` column
29
header: 'Employee',
30
columns: [
31
{
32
accessorFn: (row) => `${row.firstName} ${row.lastName}`, //accessorFn used to join multiple data into a single cell
33
id: 'name', //id is still required when using accessorFn instead of accessorKey
34
header: 'Name',
35
size: 250,
36
filterVariant: 'autocomplete',
37
Cell: ({ renderedCellValue, row }) => (
38
<Box
39
sx={{
40
display: 'flex',
41
alignItems: 'center',
42
gap: '16px',
43
}}
44
>
45
<img
46
alt="avatar"
47
height={30}
48
src={row.original.avatar}
49
style={{ borderRadius: '50%' }}
50
/>
51
<span>{renderedCellValue}</span>
52
</Box>
53
),
54
},
55
{
56
accessorKey: 'email', //accessorKey used to define `data` column. `id` gets set to accessorKey automatically
57
enableClickToCopy: true,
58
header: 'Email',
59
size: 300,
60
},
61
],
62
},
63
{
64
id: 'id',
65
header: 'Job Info',
66
columns: [
67
{
68
accessorKey: 'salary',
69
header: 'Salary',
70
size: 200,
71
filterVariant: 'range-slider',
72
mantineFilterRangeSliderProps: {
73
color: 'indigo',
74
label: (value) =>
75
value?.toLocaleString?.('en-US', {
76
style: 'currency',
77
currency: 'USD',
78
minimumFractionDigits: 0,
79
maximumFractionDigits: 0,
80
}),
81
},
82
//custom conditional format and styling
83
Cell: ({ cell }) => (
84
<Box
85
sx={(theme) => ({
86
backgroundColor:
87
cell.getValue<number>() < 50_000
88
? theme.colors.red[9]
89
: cell.getValue<number>() >= 50_000 &&
90
cell.getValue<number>() < 75_000
91
? theme.colors.yellow[9]
92
: theme.colors.green[9],
93
borderRadius: '4px',
94
color: '#fff',
95
maxWidth: '9ch',
96
padding: '4px',
97
})}
98
>
99
{cell.getValue<number>()?.toLocaleString?.('en-US', {
100
style: 'currency',
101
currency: 'USD',
102
minimumFractionDigits: 0,
103
maximumFractionDigits: 0,
104
})}
105
</Box>
106
),
107
},
108
{
109
accessorKey: 'jobTitle', //hey a simple column for once
110
header: 'Job Title',
111
filterVariant: 'multi-select',
112
size: 350,
113
},
114
{
115
accessorFn: (row) => {
116
//convert to Date for sorting and filtering
117
const sDay = new Date(row.startDate);
118
sDay.setHours(0, 0, 0, 0); // remove time from date (useful if filter by equals exact date)
119
return sDay;
120
},
121
id: 'startDate',
122
header: 'Start Date',
123
filterVariant: 'date-range',
124
sortingFn: 'datetime',
125
enableColumnFilterModes: false, //keep this as only date-range filter with between inclusive filterFn
126
Cell: ({ cell }) => cell.getValue<Date>()?.toLocaleDateString(), //render Date as a string
127
Header: ({ column }) => <em>{column.columnDef.header}</em>, //custom header markup
128
},
129
],
130
},
131
],
132
[],
133
);
134
135
const table = useMantineReactTable({
136
columns,
137
data, //must be memoized or stable (useState, useMemo, defined outside of this component, etc.)
138
enableColumnFilterModes: true,
139
enableColumnOrdering: true,
140
enableFacetedValues: true,
141
enableGrouping: true,
142
enablePinning: true,
143
enableRowActions: true,
144
enableRowSelection: true,
145
initialState: { showColumnFilters: true, showGlobalFilter: true },
146
paginationDisplayMode: 'pages',
147
positionToolbarAlertBanner: 'bottom',
148
mantinePaginationProps: {
149
radius: 'xl',
150
size: 'lg',
151
},
152
mantineSearchTextInputProps: {
153
placeholder: 'Search Employees',
154
},
155
renderDetailPanel: ({ row }) => (
156
<Box
157
sx={{
158
display: 'flex',
159
justifyContent: 'flex-start',
160
alignItems: 'center',
161
gap: '16px',
162
padding: '16px',
163
}}
164
>
165
<img
166
alt="avatar"
167
height={200}
168
src={row.original.avatar}
169
style={{ borderRadius: '50%' }}
170
/>
171
<Box sx={{ textAlign: 'center' }}>
172
<Title>Signature Catch Phrase:</Title>
173
<Text>&quot;{row.original.signatureCatchPhrase}&quot;</Text>
174
</Box>
175
</Box>
176
),
177
renderRowActionMenuItems: () => (
178
<>
179
<Menu.Item icon={<IconUserCircle />}>View Profile</Menu.Item>
180
<Menu.Item icon={<IconSend />}>Send Email</Menu.Item>
181
</>
182
),
183
renderTopToolbar: ({ table }) => {
184
const handleDeactivate = () => {
185
table.getSelectedRowModel().flatRows.map((row) => {
186
alert('deactivating ' + row.getValue('name'));
187
});
188
};
189
190
const handleActivate = () => {
191
table.getSelectedRowModel().flatRows.map((row) => {
192
alert('activating ' + row.getValue('name'));
193
});
194
};
195
196
const handleContact = () => {
197
table.getSelectedRowModel().flatRows.map((row) => {
198
alert('contact ' + row.getValue('name'));
199
});
200
};
201
202
return (
203
<Flex p="md" justify="space-between">
204
<Flex gap="xs">
205
{/* import MRT sub-components */}
206
<MRT_GlobalFilterTextInput table={table} />
207
<MRT_ToggleFiltersButton table={table} />
208
</Flex>
209
<Flex sx={{ gap: '8px' }}>
210
<Button
211
color="red"
212
disabled={!table.getIsSomeRowsSelected()}
213
onClick={handleDeactivate}
214
variant="filled"
215
>
216
Deactivate
217
</Button>
218
<Button
219
color="green"
220
disabled={!table.getIsSomeRowsSelected()}
221
onClick={handleActivate}
222
variant="filled"
223
>
224
Activate
225
</Button>
226
<Button
227
color="blue"
228
disabled={!table.getIsSomeRowsSelected()}
229
onClick={handleContact}
230
variant="filled"
231
>
232
Contact
233
</Button>
234
</Flex>
235
</Flex>
236
);
237
},
238
});
239
240
return <MantineReactTable table={table} />;
241
};
242
243
export default Example;
1
import { useMemo } from 'react';
2
import {
3
MRT_GlobalFilterTextInput,
4
MRT_ToggleFiltersButton,
5
MantineReactTable,
6
useMantineReactTable,
7
} from 'mantine-react-table';
8
import { Box, Button, Flex, Menu, Text, Title } from '@mantine/core';
9
import { IconUserCircle, IconSend } from '@tabler/icons-react';
10
import { data } from './makeData';
11
12
const Example = () => {
13
const columns = useMemo(
14
() => [
15
{
16
id: 'employee', //id used to define `group` column
17
header: 'Employee',
18
columns: [
19
{
20
accessorFn: (row) => `${row.firstName} ${row.lastName}`, //accessorFn used to join multiple data into a single cell
21
id: 'name', //id is still required when using accessorFn instead of accessorKey
22
header: 'Name',
23
size: 250,
24
filterVariant: 'autocomplete',
25
Cell: ({ renderedCellValue, row }) => (
26
<Box
27
sx={{
28
display: 'flex',
29
alignItems: 'center',
30
gap: '16px',
31
}}
32
>
33
<img
34
alt="avatar"
35
height={30}
36
src={row.original.avatar}
37
style={{ borderRadius: '50%' }}
38
/>
39
<span>{renderedCellValue}</span>
40
</Box>
41
),
42
},
43
{
44
accessorKey: 'email', //accessorKey used to define `data` column. `id` gets set to accessorKey automatically
45
enableClickToCopy: true,
46
header: 'Email',
47
size: 300,
48
},
49
],
50
},
51
{
52
id: 'id',
53
header: 'Job Info',
54
columns: [
55
{
56
accessorKey: 'salary',
57
header: 'Salary',
58
size: 200,
59
filterVariant: 'range-slider',
60
mantineFilterRangeSliderProps: {
61
color: 'indigo',
62
label: (value) =>
63
value?.toLocaleString?.('en-US', {
64
style: 'currency',
65
currency: 'USD',
66
minimumFractionDigits: 0,
67
maximumFractionDigits: 0,
68
}),
69
},
70
//custom conditional format and styling
71
Cell: ({ cell }) => (
72
<Box
73
sx={(theme) => ({
74
backgroundColor:
75
cell.getValue() < 50_000
76
? theme.colors.red[9]
77
: cell.getValue() >= 50_000 && cell.getValue() < 75_000
78
? theme.colors.yellow[9]
79
: theme.colors.green[9],
80
borderRadius: '4px',
81
color: '#fff',
82
maxWidth: '9ch',
83
padding: '4px',
84
})}
85
>
86
{cell.getValue()?.toLocaleString?.('en-US', {
87
style: 'currency',
88
currency: 'USD',
89
minimumFractionDigits: 0,
90
maximumFractionDigits: 0,
91
})}
92
</Box>
93
),
94
},
95
{
96
accessorKey: 'jobTitle', //hey a simple column for once
97
header: 'Job Title',
98
filterVariant: 'multi-select',
99
size: 350,
100
},
101
{
102
accessorFn: (row) => {
103
//convert to Date for sorting and filtering
104
const sDay = new Date(row.startDate);
105
sDay.setHours(0, 0, 0, 0); // remove time from date (useful if filter by equals exact date)
106
return sDay;
107
},
108
id: 'startDate',
109
header: 'Start Date',
110
filterVariant: 'date-range',
111
sortingFn: 'datetime',
112
enableColumnFilterModes: false, //keep this as only date-range filter with between inclusive filterFn
113
Cell: ({ cell }) => cell.getValue()?.toLocaleDateString(), //render Date as a string
114
Header: ({ column }) => <em>{column.columnDef.header}</em>, //custom header markup
115
},
116
],
117
},
118
],
119
[],
120
);
121
122
const table = useMantineReactTable({
123
columns,
124
data, //must be memoized or stable (useState, useMemo, defined outside of this component, etc.)
125
enableColumnFilterModes: true,
126
enableColumnOrdering: true,
127
enableFacetedValues: true,
128
enableGrouping: true,
129
enablePinning: true,
130
enableRowActions: true,
131
enableRowSelection: true,
132
initialState: { showColumnFilters: true, showGlobalFilter: true },
133
paginationDisplayMode: 'pages',
134
positionToolbarAlertBanner: 'bottom',
135
mantinePaginationProps: {
136
radius: 'xl',
137
size: 'lg',
138
},
139
mantineSearchTextInputProps: {
140
placeholder: 'Search Employees',
141
},
142
renderDetailPanel: ({ row }) => (
143
<Box
144
sx={{
145
display: 'flex',
146
justifyContent: 'flex-start',
147
alignItems: 'center',
148
gap: '16px',
149
padding: '16px',
150
}}
151
>
152
<img
153
alt="avatar"
154
height={200}
155
src={row.original.avatar}
156
style={{ borderRadius: '50%' }}
157
/>
158
<Box sx={{ textAlign: 'center' }}>
159
<Title>Signature Catch Phrase:</Title>
160
<Text>&quot;{row.original.signatureCatchPhrase}&quot;</Text>
161
</Box>
162
</Box>
163
),
164
renderRowActionMenuItems: () => (
165
<>
166
<Menu.Item icon={<IconUserCircle />}>View Profile</Menu.Item>
167
<Menu.Item icon={<IconSend />}>Send Email</Menu.Item>
168
</>
169
),
170
renderTopToolbar: ({ table }) => {
171
const handleDeactivate = () => {
172
table.getSelectedRowModel().flatRows.map((row) => {
173
alert('deactivating ' + row.getValue('name'));
174
});
175
};
176
177
const handleActivate = () => {
178
table.getSelectedRowModel().flatRows.map((row) => {
179
alert('activating ' + row.getValue('name'));
180
});
181
};
182
183
const handleContact = () => {
184
table.getSelectedRowModel().flatRows.map((row) => {
185
alert('contact ' + row.getValue('name'));
186
});
187
};
188
189
return (
190
<Flex p="md" justify="space-between">
191
<Flex gap="xs">
192
{/* import MRT sub-components */}
193
<MRT_GlobalFilterTextInput table={table} />
194
<MRT_ToggleFiltersButton table={table} />
195
</Flex>
196
<Flex sx={{ gap: '8px' }}>
197
<Button
198
color="red"
199
disabled={!table.getIsSomeRowsSelected()}
200
onClick={handleDeactivate}
201
variant="filled"
202
>
203
Deactivate
204
</Button>
205
<Button
206
color="green"
207
disabled={!table.getIsSomeRowsSelected()}
208
onClick={handleActivate}
209
variant="filled"
210
>
211
Activate
212
</Button>
213
<Button
214
color="blue"
215
disabled={!table.getIsSomeRowsSelected()}
216
onClick={handleContact}
217
variant="filled"
218
>
219
Contact
220
</Button>
221
</Flex>
222
</Flex>
223
);
224
},
225
});
226
227
return <MantineReactTable table={table} />;
228
};
229
230
export default Example;
1
import { useMemo } from 'react';
2
import { MantineReactTable, type MRT_ColumnDef } from 'mantine-react-table';
3
import { Box, Button, Menu, Text, Title } from '@mantine/core';
4
import { IconUserCircle, IconSend } from '@tabler/icons-react';
5
import { data } from './makeData';
6
7
export type Employee = {
8
firstName: string;
9
lastName: string;
10
email: string;
11
jobTitle: string;
12
salary: number;
13
startDate: string;
14
signatureCatchPhrase: string;
15
avatar: string;
16
};
17
18
const Example = () => {
19
const columns = useMemo<MRT_ColumnDef<Employee>[]>(
20
() => [
21
{
22
id: 'employee', //id used to define `group` column
23
header: 'Employee',
24
columns: [
25
{
26
accessorFn: (row) => `${row.firstName} ${row.lastName}`, //accessorFn used to join multiple data into a single cell
27
id: 'name', //id is still required when using accessorFn instead of accessorKey
28
header: 'Name',
29
size: 250,
30
filterVariant: 'autocomplete',
31
Cell: ({ renderedCellValue, row }) => (
32
<Box
33
sx={{
34
display: 'flex',
35
alignItems: 'center',
36
gap: '16px',
37
}}
38
>
39
<img
40
alt="avatar"
41
height={30}
42
src={row.original.avatar}
43
style={{ borderRadius: '50%' }}
44
/>
45
<span>{renderedCellValue}</span>
46
</Box>
47
),
48
},
49
{
50
accessorKey: 'email', //accessorKey used to define `data` column. `id` gets set to accessorKey automatically
51
enableClickToCopy: true,
52
header: 'Email',
53
size: 300,
54
},
55
],
56
},
57
{
58
id: 'id',
59
header: 'Job Info',
60
columns: [
61
{
62
accessorKey: 'salary',
63
header: 'Salary',
64
size: 200,
65
filterVariant: 'range-slider',
66
mantineFilterRangeSliderProps: {
67
color: 'indigo',
68
label: (value) =>
69
value?.toLocaleString?.('en-US', {
70
style: 'currency',
71
currency: 'USD',
72
minimumFractionDigits: 0,
73
maximumFractionDigits: 0,
74
}),
75
},
76
//custom conditional format and styling
77
Cell: ({ cell }) => (
78
<Box
79
sx={(theme) => ({
80
backgroundColor:
81
cell.getValue<number>() < 50_000
82
? theme.colors.red[9]
83
: cell.getValue<number>() >= 50_000 &&
84
cell.getValue<number>() < 75_000
85
? theme.colors.yellow[9]
86
: theme.colors.green[9],
87
borderRadius: '4px',
88
color: '#fff',
89
maxWidth: '9ch',
90
padding: '4px',
91
})}
92
>
93
{cell.getValue<number>()?.toLocaleString?.('en-US', {
94
style: 'currency',
95
currency: 'USD',
96
minimumFractionDigits: 0,
97
maximumFractionDigits: 0,
98
})}
99
</Box>
100
),
101
},
102
{
103
accessorKey: 'jobTitle', //hey a simple column for once
104
header: 'Job Title',
105
filterVariant: 'multi-select',
106
size: 350,
107
},
108
{
109
accessorFn: (row) => {
110
//convert to Date for sorting and filtering
111
const sDay = new Date(row.startDate);
112
sDay.setHours(0, 0, 0, 0); // remove time from date (useful if filter by equals exact date)
113
return sDay;
114
},
115
id: 'startDate',
116
header: 'Start Date',
117
filterVariant: 'date-range',
118
sortingFn: 'datetime',
119
enableColumnFilterModes: false, //keep this as only date-range filter with between inclusive filterFn
120
Cell: ({ cell }) => cell.getValue<Date>()?.toLocaleDateString(), //render Date as a string
121
Header: ({ column }) => <em>{column.columnDef.header}</em>, //custom header markup
122
},
123
],
124
},
125
],
126
[],
127
);
128
129
return (
130
<MantineReactTable
131
columns={columns}
132
data={data}
133
enableColumnFilterModes
134
enableColumnOrdering
135
enableGrouping
136
enablePinning
137
enableRowActions
138
enableRowSelection
139
initialState={{ showColumnFilters: true }}
140
paginationDisplayMode="pages"
141
positionToolbarAlertBanner="bottom"
142
renderDetailPanel={({ row }) => (
143
<Box
144
sx={{
145
display: 'flex',
146
justifyContent: 'flex-start',
147
alignItems: 'center',
148
gap: '16px',
149
padding: '16px',
150
}}
151
>
152
<img
153
alt="avatar"
154
height={200}
155
src={row.original.avatar}
156
style={{ borderRadius: '50%' }}
157
/>
158
<Box sx={{ textAlign: 'center' }}>
159
<Title>Signature Catch Phrase:</Title>
160
<Text>&quot;{row.original.signatureCatchPhrase}&quot;</Text>
161
</Box>
162
</Box>
163
)}
164
renderRowActionMenuItems={() => (
165
<>
166
<Menu.Item icon={<IconUserCircle />}>View Profile</Menu.Item>
167
<Menu.Item icon={<IconSend />}>Send Email</Menu.Item>
168
</>
169
)}
170
renderTopToolbarCustomActions={({ table }) => {
171
const handleDeactivate = () => {
172
table.getSelectedRowModel().flatRows.map((row) => {
173
alert('deactivating ' + row.getValue('name'));
174
});
175
};
176
177
const handleActivate = () => {
178
table.getSelectedRowModel().flatRows.map((row) => {
179
alert('activating ' + row.getValue('name'));
180
});
181
};
182
183
const handleContact = () => {
184
table.getSelectedRowModel().flatRows.map((row) => {
185
alert('contact ' + row.getValue('name'));
186
});
187
};
188
189
return (
190
<div style={{ display: 'flex', gap: '8px' }}>
191
<Button
192
color="red"
193
disabled={!table.getIsSomeRowsSelected()}
194
onClick={handleDeactivate}
195
variant="filled"
196
>
197
Deactivate
198
</Button>
199
<Button
200
color="green"
201
disabled={!table.getIsSomeRowsSelected()}
202
onClick={handleActivate}
203
variant="filled"
204
>
205
Activate
206
</Button>
207
<Button
208
color="blue"
209
disabled={!table.getIsSomeRowsSelected()}
210
onClick={handleContact}
211
variant="filled"
212
>
213
Contact
214
</Button>
215
</div>
216
);
217
}}
218
/>
219
);
220
};
221
222
export default Example;
View Extra Storybook Examples
You can help make these docs better! PRs are Welcome
Using Material-UI instead of Mantine?
Check out Material React Table