MRT logoMaterial React Table

Detail Panel Feature Guide

Material React Table has multiple kinds of expanding features. This guide will show you how to use the detail panel feature to expand a single row to show more information for that row.

If you are looking for how to expand multiple rows from a tree data structure, see the Expanding Sub-Rows guide.

Relevant Table Options

1
{ [key: string]: MRT_DisplayColumnDef<TData> }
MRT Display Columns Docs
2
boolean
true
MRT Expanding Sub Rows Docs
3
TableCellProps | ({ row, table }) => TableCellProps
Material UI TableCell Props
4
IconButtonProps | ({ table }) => IconButtonProps
Material UI IconButton Props
5
IconButtonProps | ({ row, table }) => IconButtonProps
Material UI IconButton Props
6
'first' | 'last'
'first'
7
({ row, table }) => ReactNode

Relevant State

1
Record<string, boolean> | boolean
{}
TanStack Table Expanding Docs

Render Detail Panel

To add a detail panel to a row, all you need to do is add a renderDetailPanel table option.

The recommended way to access the row data for the detail panel is to pull from the original object on a row. This gives you the original data for the row, not transformed or filtered by TanStack Table.

Using row.getValue('columnId') will not work for data that does not have its own column. Using row.original.columnId is recommended for detail panels since the data in the detail panel usually does not have its own column.

Disable Expand All Button

If you don't want to show the expand all button, you can set the enableExpandAll table option to false.

const table = useMaterialReactTable({
data,
columns,
enableExpandAll: false,
});

Enable Detail Panel Conditionally Per Row

If the return value of your renderDetailPanel function returns null or a falsy value for a row, the expand button will be disabled for that row.

const table = useMaterialReactTable({
columns,
data,
renderDetailPanel: ({ row }) =>
row.original.someCondition ? <DetailPanelContent /> : null,
});

One thing to note about the implementation of conditional detail panels is that additional <tr> elements will still be created for all rows, even if they do not have detail panel content. It is implemented this way in order to avoid bugs with row virtualization, or striped row CSS.

Only Allow One Detail Panel Open At A Time

If you want to only allow one detail panel to be open at a time, all you have to do is add your own onClick logic to the muiExpandButtonProps table option.

const table = useMaterialReactTable({
data,
columns,
renderDetailPanel: ({ row }) => <DetailPanelContent />,
muiExpandButtonProps: ({ row, table }) => ({
onClick: () => table.setExpanded({ [row.id]: !row.getIsExpanded() }), //set only this row to be expanded
}),
});

Rotate Expand Icon

If you don't like the default rotation styles for the expand icons, you can pass in custom CSS to the muiExpandButtonProps and muiExpandAllButtonProps table options.

Replace Expand Icon

You can easily use a custom expand icon either by following the Custom Icons Guide or by passing in custom children to the muiExpandButtonProps and muiExpandAllButtonProps table options.

const table = useMaterialReactTable({
data,
columns,
// icons, //or manage icons globally
muiExpandButtonProps: ({ row }) => ({
children: row.getIsExpanded() ? <MinusIcon /> : <AddIcon />,
}),
});

Customize or Style Detail Panel

You can use the muiDetailPanelProps table option to pass in custom props to the detail panel. These props are passed to the <td> element that contains the detail panel content.

If you need to customize the <tr> element containing the detail panel cell, you can just use the muiTableBodyRowProps table option that you use for customizing all rows. There is a isDetailPanel parameter that is available to you to target only detail panel rows.

const table = useMaterialReactTable({
data,
columns,
muiDetailPanelProps: ({ row }) => ({
sx: {
//...
},
}),
muiTableBodyRowProps: ({ isDetailPanel, row }) => ({
sx: {
// isDetailPanel ? ... : ...
},
}),
});
1DylanSprouseMurray
2RaquelHakeemKohler
3ErvinKrisReinger
4BrittanyKathrynMcCullough
5BransonJohnFrami
1-5 of 5

Source Code

1import { useMemo } from 'react';
2import {
3 MaterialReactTable,
4 useMaterialReactTable,
5 type MRT_ColumnDef,
6} from 'material-react-table';
7import { Box, Typography } from '@mui/material';
8import { data, type Person } from './makeData';
9
10const Example = () => {
11 const columns = useMemo<MRT_ColumnDef<Person>[]>(
12 //column definitions...
34 );
35
36 const table = useMaterialReactTable({
37 columns,
38 data,
39 enableExpandAll: false, //disable expand all button
40 muiDetailPanelProps: () => ({
41 sx: (theme) => ({
42 backgroundColor:
43 theme.palette.mode === 'dark'
44 ? 'rgba(255,210,244,0.1)'
45 : 'rgba(0,0,0,0.1)',
46 }),
47 }),
48 //custom expand button rotation
49 muiExpandButtonProps: ({ row, table }) => ({
50 onClick: () => table.setExpanded({ [row.id]: !row.getIsExpanded() }), //only 1 detail panel open at a time
51 sx: {
52 transform: row.getIsExpanded() ? 'rotate(180deg)' : 'rotate(-90deg)',
53 transition: 'transform 0.2s',
54 },
55 }),
56 //conditionally render detail panel
57 renderDetailPanel: ({ row }) =>
58 row.original.address ? (
59 <Box
60 sx={{
61 display: 'grid',
62 margin: 'auto',
63 gridTemplateColumns: '1fr 1fr',
64 width: '100%',
65 }}
66 >
67 <Typography>Address: {row.original.address}</Typography>
68 <Typography>City: {row.original.city}</Typography>
69 <Typography>State: {row.original.state}</Typography>
70 <Typography>Country: {row.original.country}</Typography>
71 </Box>
72 ) : null,
73 });
74
75 return <MaterialReactTable table={table} />;
76};
77
78export default Example;
79

Expand Detail Panel By Default

If you want some or all rows to be expanded by default, you can specify that in the initialState.expanded table option. Pass true to expand all rows, or specify which rowIds should be expanded.

const table = useMaterialReactTable({
data,
columns,
initialState: {
expanded: true,
// or expand specific rows by default
// expanded: {
// 1: true,
// 2: true,
// },
},
});

Position Expand Column Last

If you want to position the expand column last, you can set the positionExpandColumn table option to 'last'.

Alternatively though, you could use the Column Pinning Feature to pin the expand column to the right side of the table.

1DylanSprouseMurray

Address: 261 Erdman Ford

City: East Daphne

State: Kentucky

Country: United States

2RaquelHakeemKohler

Address: 769 Dominic Grove

City: Vancouver

State: British Columbia

Country: Canada

3ErvinKrisReinger

Address: 566 Brakus Inlet

City: South Linda

State: West Virginia

Country: United States

1-3 of 3

Source Code

1import { useMemo } from 'react';
2import {
3 MaterialReactTable,
4 useMaterialReactTable,
5 type MRT_ColumnDef,
6} from 'material-react-table';
7import { Box, Typography, useMediaQuery } from '@mui/material';
8import { data, type Person } from './makeData';
9
10const Example = () => {
11 const isMobile = useMediaQuery('(max-width: 720px)');
12
13 const columns = useMemo<MRT_ColumnDef<Person>[]>(
14 //column definitions...
36 );
37
38 const table = useMaterialReactTable({
39 columns,
40 data,
41 // displayColumnDefOptions: { //built-in now in v2.6.0 when positionExpandColumn is 'last'
42 // 'mrt-row-expand': {
43 // muiTableHeadCellProps: {
44 // align: 'right',
45 // },
46 // muiTableBodyCellProps: {
47 // align: 'right',
48 // },
49 // },
50 // },
51 enableColumnPinning: isMobile, //alternative
52 initialState: {
53 expanded: true,
54 },
55 state: {
56 columnPinning: isMobile ? { right: ['mrt-row-expand'] } : {}, //alternative
57 },
58 renderDetailPanel: ({ row }) => (
59 <Box
60 sx={{
61 display: 'grid',
62 margin: 'auto',
63 gridTemplateColumns: '1fr 1fr',
64 width: '100%',
65 }}
66 >
67 <Typography>Address: {row.original.address}</Typography>
68 <Typography>City: {row.original.city}</Typography>
69 <Typography>State: {row.original.state}</Typography>
70 <Typography>Country: {row.original.country}</Typography>
71 </Box>
72 ),
73 positionExpandColumn: 'last',
74 });
75
76 return <MaterialReactTable table={table} />;
77};
78
79export default Example;
80

Detail Panel With Charts

The detail panel can be used to show a variety of content. Here's an example of a detail panel rendering charts with the MUI X Charts library.

1DylanSprouseMurray
#1#2#3#4#5#6#7#8#9#10Games Played0204060PointsAssistsTurnovers
2RaquelHakeemKohler
3ErvinKrisReinger
4BrittanyKathrynMcCullough
5BransonJohnFrami
1-5 of 5

Source Code

1import { useMemo } from 'react';
2import {
3 MaterialReactTable,
4 useMaterialReactTable,
5 type MRT_ColumnDef,
6} from 'material-react-table';
7import { useTheme } from '@mui/material';
8import { LineChart } from '@mui/x-charts/LineChart';
9import { data, type Person } from './makeData';
10
11const Example = () => {
12 const theme = useTheme();
13
14 const columns = useMemo<MRT_ColumnDef<Person>[]>(
15 //column definitions...
37 );
38
39 const table = useMaterialReactTable({
40 columns,
41 data,
42 initialState: { expanded: { 0: true } },
43 muiTableBodyRowProps: {
44 sx: {
45 '.Mui-TableBodyCell-DetailPanel': {
46 backgroundColor:
47 theme.palette.mode === 'dark'
48 ? theme.palette.grey[900]
49 : theme.palette.grey[100],
50 },
51 },
52 },
53 renderDetailPanel: ({ row }) => (
54 <LineChart
55 xAxis={[
56 {
57 data: row.original.gamesPlayed,
58 label: 'Games Played',
59 valueFormatter: (value) => `#${value}`,
60 tickLabelInterval: (value) => value % 1 === 0,
61 },
62 ]}
63 yAxis={[{ min: 0, max: 60 }]}
64 series={[
65 {
66 color: theme.palette.primary.dark,
67 data: row.original.points,
68 label: 'Points',
69 },
70 {
71 color: theme.palette.secondary.main,
72 data: row.original.assists,
73 label: 'Assists',
74 },
75 {
76 color: theme.palette.error.main,
77 data: row.original.turnovers,
78 label: 'Turnovers',
79 },
80 ]}
81 height={250}
82 />
83 ),
84 });
85
86 return <MaterialReactTable table={table} />;
87};
88
89export default Example;
90

Detail Panels with Virtualization

New in v2.6.0

If you are using row virtualization, detail panels will now work more properly as of version 2.6.0. However, there are some caveats to be aware of. In order for row virtualization to work well, many of the animation/transitions have been disabled. This means that the detail panel will not animate open and closed. It will simply appear and disappear.

You also may need to specify some more accurate row height estimations for the row virtualizer in order to achieve the best scrollbar behavior. See the Row Virtualization Guide for the full details on this topic, but here's an example of how you might do that.

const table = useMaterialReactTable({
data,
columns,
enableRowVirtualization: true,
renderDetailPanel: ({ row }) => <DetailPanelContent />,
rowVirtualizerOptions: ({ table }) => {
const { density, expanded } = table.getState();
return {
//adjust to your needs
estimateSize: (index) =>
index % 2 === 1 //even rows are normal rows, odd rows are detail panels
? //Estimate open detail panels as 80px tall, closed detail panels as 0px tall
expanded === true
? 80
: 0
: //estimate normal row heights
density === 'compact'
? 37
: density === 'comfortable'
? 58
: 73,
};
},
});
MasonAndersonmanderson57@yopmail.com
NoraBishopnbishop26@mailinator.com
LiamPattersonlpatterson61@yopmail.com
HarperRosshross38@mailinator.com
OliverBakerobaker72@yopmail.com
CharlottePhillipscphillips33@mailinator.com
HenryCooperhcooper18@yopmail.com
EmmaJenkinsejenkins49@mailinator.com
AlexanderGonzalezagonzalez67@yopmail.com
AvaRamirezaramirez94@mailinator.com
WilliamBaileywbailey59@yopmail.com
SophiaCoxscox77@mailinator.com

Source Code

1import { useMemo } from 'react';
2import {
3 MaterialReactTable,
4 useMaterialReactTable,
5 type MRT_ColumnDef,
6} from 'material-react-table';
7import { Box, Typography } from '@mui/material';
8import { data, type Person } from './makeData';
9
10const Example = () => {
11 const columns = useMemo<MRT_ColumnDef<Person>[]>(
12 //column definitions...
29 );
30
31 const table = useMaterialReactTable({
32 columns,
33 data,
34 enableBottomToolbar: false,
35 enablePagination: false,
36 enableRowVirtualization: true,
37 muiTableContainerProps: {
38 sx: {
39 maxHeight: '500px',
40 },
41 },
42 renderDetailPanel: ({ row }) => (
43 <Box
44 sx={{
45 display: 'grid',
46 margin: 'auto',
47 gridTemplateColumns: '1fr 1fr',
48 width: '100%',
49 }}
50 >
51 <Typography>Address: {row.original.address}</Typography>
52 <Typography>City: {row.original.city}</Typography>
53 <Typography>State: {row.original.state}</Typography>
54 <Typography>Country: {row.original.country}</Typography>
55 </Box>
56 ),
57 rowVirtualizerOptions: ({ table }) => {
58 const { density, expanded } = table.getState();
59 return {
60 //adjust to your needs
61 estimateSize: (index) =>
62 index % 2 === 1 //even rows are normal rows, odd rows are detail panels
63 ? //Estimate open detail panels as 80px tall, closed detail panels as 0px tall
64 expanded === true
65 ? 80
66 : 0
67 : //estimate normal row heights
68 density === 'compact'
69 ? 37
70 : density === 'comfortable'
71 ? 58
72 : 73,
73 };
74 },
75 });
76
77 return <MaterialReactTable table={table} />;
78};
79
80export default Example;
81

Lazy Detail Panels

Fetching the additional data for the detail panels only after the user clicks to expand the row can be a good way to improve performance, and it is pretty easy to implement. It's even easier if you are using React Query.

0-0 of 0

Source Code

1import { useMemo, useState } from 'react';
2import {
3 MaterialReactTable,
4 useMaterialReactTable,
5 type MRT_ColumnDef,
6 type MRT_ColumnFiltersState,
7 type MRT_PaginationState,
8 type MRT_SortingState,
9 type MRT_Row,
10} from 'material-react-table';
11import { Alert, CircularProgress, Stack } from '@mui/material';
12import AddIcon from '@mui/icons-material/Add';
13import MinusIcon from '@mui/icons-material/Remove';
14import {
15 QueryClient,
16 QueryClientProvider,
17 keepPreviousData,
18 useQuery,
19} from '@tanstack/react-query'; //note: this is TanStack React Query V5
20
21//Your API response shape will probably be different. Knowing a total row count is important though.
22type UserApiResponse = {
23 data: Array<User>;
24 meta: {
25 totalRowCount: number;
26 };
27};
28
29type User = {
30 firstName: string;
31 lastName: string;
32 address: string;
33 state: string;
34 phoneNumber: string;
35 lastLogin: Date;
36};
37
38type FullUserInfoApiResponse = FullUserInfo;
39
40type FullUserInfo = User & {
41 favoriteMusic: string;
42 favoriteSong: string;
43 quote: string;
44};
45
46const DetailPanel = ({ row }: { row: MRT_Row<User> }) => {
47 const {
48 data: userInfo,
49 isLoading,
50 isError,
51 } = useFetchUserInfo(
52 {
53 phoneNumber: row.id, //the row id is set to the user's phone number
54 },
55 {
56 enabled: row.getIsExpanded(),
57 },
58 );
59 if (isLoading) return <CircularProgress />;
60 if (isError) return <Alert severity="error">Error Loading User Info</Alert>;
61
62 const { favoriteMusic, favoriteSong, quote } = userInfo ?? {};
63
64 return (
65 <Stack gap="0.5rem" minHeight="00px">
66 <div>
67 <b>Favorite Music:</b> {favoriteMusic}
68 </div>
69 <div>
70 <b>Favorite Song:</b> {favoriteSong}
71 </div>
72 <div>
73 <b>Quote:</b> {quote}
74 </div>
75 </Stack>
76 );
77};
78
79const Example = () => {
80 //manage our own state for stuff we want to pass to the API
81 const [columnFilters, setColumnFilters] = useState<MRT_ColumnFiltersState>(
82 [],
83 );
84 const [globalFilter, setGlobalFilter] = useState('');
85 const [sorting, setSorting] = useState<MRT_SortingState>([]);
86 const [pagination, setPagination] = useState<MRT_PaginationState>({
87 pageIndex: 0,
88 pageSize: 5,
89 });
90
91 const {
92 data: { data = [], meta } = {},
93 isError,
94 isRefetching,
95 isLoading,
96 } = useFetchUsers({
97 columnFilters,
98 globalFilter,
99 pagination,
100 sorting,
101 });
102
103 const columns = useMemo<MRT_ColumnDef<User>[]>(
104 //column definitions...
129 );
130
131 const table = useMaterialReactTable({
132 columns,
133 data,
134 getRowId: (row) => row.phoneNumber,
135 manualFiltering: true, //turn off built-in client-side filtering
136 manualPagination: true, //turn off built-in client-side pagination
137 manualSorting: true, //turn off built-in client-side sorting
138 muiExpandButtonProps: ({ row }) => ({
139 children: row.getIsExpanded() ? <MinusIcon /> : <AddIcon />,
140 }),
141 muiToolbarAlertBannerProps: isError
142 ? {
143 color: 'error',
144 children: 'Error loading data',
145 }
146 : undefined,
147 onColumnFiltersChange: setColumnFilters,
148 onGlobalFilterChange: setGlobalFilter,
149 onPaginationChange: setPagination,
150 onSortingChange: setSorting,
151 renderDetailPanel: ({ row }) => <DetailPanel row={row} />,
152 rowCount: meta?.totalRowCount ?? 0,
153 state: {
154 columnFilters,
155 globalFilter,
156 isLoading,
157 pagination,
158 showAlertBanner: isError,
159 showProgressBars: isRefetching,
160 sorting,
161 },
162 });
163
164 return <MaterialReactTable table={table} />;
165};
166
167const queryClient = new QueryClient();
168
169const ExampleWithReactQueryProvider = () => (
170 //App.tsx or AppProviders file. Don't just wrap this component with QueryClientProvider! Wrap your whole App!
171 <QueryClientProvider client={queryClient}>
172 <Example />
173 </QueryClientProvider>
174);
175
176export default ExampleWithReactQueryProvider;
177
178//fetch user hook
179const useFetchUsers = ({
180 columnFilters,
181 globalFilter,
182 pagination,
183 sorting,
184}: {
185 columnFilters: MRT_ColumnFiltersState;
186 globalFilter: string;
187 pagination: MRT_PaginationState;
188 sorting: MRT_SortingState;
189}) => {
190 return useQuery<UserApiResponse>({
191 queryKey: [
192 'users', //give a unique key for this query
193 columnFilters, //refetch when columnFilters changes
194 globalFilter, //refetch when globalFilter changes
195 pagination.pageIndex, //refetch when pagination.pageIndex changes
196 pagination.pageSize, //refetch when pagination.pageSize changes
197 sorting, //refetch when sorting changes
198 ],
199 queryFn: async () => {
200 const fetchURL = new URL(
201 '/api/data',
202 process.env.NODE_ENV === 'production'
203 ? 'https://www.material-react-table.com'
204 : 'http://localhost:3000',
205 );
206
207 //read our state and pass it to the API as query params
208 fetchURL.searchParams.set(
209 'start',
210 `${pagination.pageIndex * pagination.pageSize}`,
211 );
212 fetchURL.searchParams.set('size', `${pagination.pageSize}`);
213 fetchURL.searchParams.set('filters', JSON.stringify(columnFilters ?? []));
214 fetchURL.searchParams.set('globalFilter', globalFilter ?? '');
215 fetchURL.searchParams.set('sorting', JSON.stringify(sorting ?? []));
216
217 //use whatever fetch library you want, fetch, axios, etc
218 const response = await fetch(fetchURL.href);
219 const json = (await response.json()) as UserApiResponse;
220 return json;
221 },
222 placeholderData: keepPreviousData, //don't go to 0 rows when refetching or paginating to next page
223 });
224};
225
226//fetch more user info hook
227const useFetchUserInfo = (
228 params: { phoneNumber: string },
229 options: { enabled: boolean },
230) => {
231 return useQuery<FullUserInfoApiResponse>({
232 enabled: options.enabled, //only fetch when the detail panel is opened
233 staleTime: 60 * 1000, //don't refetch for 60 seconds
234 queryKey: ['user', params.phoneNumber], //give a unique key for this query for each user fetch
235 queryFn: async () => {
236 const fetchURL = new URL(
237 `/api/moredata/${params.phoneNumber
238 .replaceAll('-', '')
239 .replaceAll('.', '')
240 .replaceAll('(', '')
241 .replaceAll(')', '')}`,
242 process.env.NODE_ENV === 'production'
243 ? 'https://www.material-react-table.com'
244 : 'http://localhost:3000',
245 );
246
247 //use whatever fetch library you want, fetch, axios, etc
248 const response = await fetch(fetchURL.href);
249 const json = (await response.json()) as FullUserInfoApiResponse;
250 return json;
251 },
252 });
253};
254

View Extra Storybook Examples