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
# | Prop Name | Type | Default Value | More Info Links | |
---|---|---|---|---|---|
1 |
| MRT Display Columns Docs | |||
2 |
|
| MRT Expanding Sub Rows Docs | ||
3 |
| Material UI TableCell Props | |||
4 |
| Material UI IconButton Props | |||
5 |
| Material UI IconButton Props | |||
6 |
|
| |||
7 |
| ||||
Relevant State
# | State Option | Type | Default Value | More Info Links | |
---|---|---|---|---|---|
1 |
|
| 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. Usingrow.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 globallymuiExpandButtonProps: ({ 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 ? ... : ...},}),});
Expand | ID | First Name | Middle Name | Last Name |
---|---|---|---|---|
1 | Dylan | Sprouse | Murray | |
2 | Raquel | Hakeem | Kohler | |
3 | Ervin | Kris | Reinger | |
4 | Brittany | Kathryn | McCullough | |
5 | Branson | John | Frami | |
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';910const Example = () => {11 const columns = useMemo<MRT_ColumnDef<Person>[]>(12 //column definitions...34 );3536 const table = useMaterialReactTable({37 columns,38 data,39 enableExpandAll: false, //disable expand all button40 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 rotation49 muiExpandButtonProps: ({ row, table }) => ({50 onClick: () => table.setExpanded({ [row.id]: !row.getIsExpanded() }), //only 1 detail panel open at a time51 sx: {52 transform: row.getIsExpanded() ? 'rotate(180deg)' : 'rotate(-90deg)',53 transition: 'transform 0.2s',54 },55 }),56 //conditionally render detail panel57 renderDetailPanel: ({ row }) =>58 row.original.address ? (59 <Box60 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 });7475 return <MaterialReactTable table={table} />;76};7778export 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.
ID | First Name | Middle Name | Last Name | |
---|---|---|---|---|
1 | Dylan | Sprouse | Murray | |
Address: 261 Erdman Ford City: East Daphne State: Kentucky Country: United States | ||||
2 | Raquel | Hakeem | Kohler | |
Address: 769 Dominic Grove City: Vancouver State: British Columbia Country: Canada | ||||
3 | Ervin | Kris | Reinger | |
Address: 566 Brakus Inlet City: South Linda State: West Virginia Country: United States |
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';910const Example = () => {11 const isMobile = useMediaQuery('(max-width: 720px)');1213 const columns = useMemo<MRT_ColumnDef<Person>[]>(14 //column definitions...36 );3738 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, //alternative52 initialState: {53 expanded: true,54 },55 state: {56 columnPinning: isMobile ? { right: ['mrt-row-expand'] } : {}, //alternative57 },58 renderDetailPanel: ({ row }) => (59 <Box60 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 });7576 return <MaterialReactTable table={table} />;77};7879export 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.
ID | First Name | Middle Name | Last Name | |
---|---|---|---|---|
1 | Dylan | Sprouse | Murray | |
2 | Raquel | Hakeem | Kohler | |
3 | Ervin | Kris | Reinger | |
4 | Brittany | Kathryn | McCullough | |
5 | Branson | John | Frami | |
1import { useMemo } from 'react';2import {3 MaterialReactTable,4 useMaterialReactTable,5 type MRT_ColumnDef,6} from 'material-react-table';7import { useTheme } from '@mui/material/styles';8import { LineChart } from '@mui/x-charts/LineChart';9import { data, type Person } from './makeData';1011const Example = () => {12 const theme = useTheme();1314 const columns = useMemo<MRT_ColumnDef<Person>[]>(15 //column definitions...37 );3839 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 <LineChart55 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 });8586 return <MaterialReactTable table={table} />;87};8889export 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 needsestimateSize: (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 tallexpanded === true? 80: 0: //estimate normal row heightsdensity === 'compact'? 37: density === 'comfortable'? 58: 73,};},});
First Name | Last Name | Email |
---|
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';910const Example = () => {11 const columns = useMemo<MRT_ColumnDef<Person>[]>(12 //column definitions...29 );3031 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 <Box44 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 needs61 estimateSize: (index) =>62 index % 2 === 1 //even rows are normal rows, odd rows are detail panels63 ? //Estimate open detail panels as 80px tall, closed detail panels as 0px tall64 expanded === true65 ? 8066 : 067 : //estimate normal row heights68 density === 'compact'69 ? 3770 : density === 'comfortable'71 ? 5872 : 73,73 };74 },75 });7677 return <MaterialReactTable table={table} />;78};7980export 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.
First Name | Last Name | Address | State | Phone Number | |
---|---|---|---|---|---|
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 V52021//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};2829type User = {30 firstName: string;31 lastName: string;32 address: string;33 state: string;34 phoneNumber: string;35 lastLogin: Date;36};3738type FullUserInfoApiResponse = FullUserInfo;3940type FullUserInfo = User & {41 favoriteMusic: string;42 favoriteSong: string;43 quote: string;44};4546const 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 number54 },55 {56 enabled: row.getIsExpanded(),57 },58 );59 if (isLoading) return <CircularProgress />;60 if (isError) return <Alert severity="error">Error Loading User Info</Alert>;6162 const { favoriteMusic, favoriteSong, quote } = userInfo ?? {};6364 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};7879const Example = () => {80 //manage our own state for stuff we want to pass to the API81 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 });9091 const {92 data: { data = [], meta } = {},93 isError,94 isRefetching,95 isLoading,96 } = useFetchUsers({97 columnFilters,98 globalFilter,99 pagination,100 sorting,101 });102103 const columns = useMemo<MRT_ColumnDef<User>[]>(104 //column definitions...129 );130131 const table = useMaterialReactTable({132 columns,133 data,134 getRowId: (row) => row.phoneNumber,135 manualFiltering: true, //turn off built-in client-side filtering136 manualPagination: true, //turn off built-in client-side pagination137 manualSorting: true, //turn off built-in client-side sorting138 muiExpandButtonProps: ({ row }) => ({139 children: row.getIsExpanded() ? <MinusIcon /> : <AddIcon />,140 }),141 muiToolbarAlertBannerProps: isError142 ? {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 });163164 return <MaterialReactTable table={table} />;165};166167const queryClient = new QueryClient();168169const 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);175176export default ExampleWithReactQueryProvider;177178//fetch user hook179const 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 query193 columnFilters, //refetch when columnFilters changes194 globalFilter, //refetch when globalFilter changes195 pagination.pageIndex, //refetch when pagination.pageIndex changes196 pagination.pageSize, //refetch when pagination.pageSize changes197 sorting, //refetch when sorting changes198 ],199 queryFn: async () => {200 const fetchURL = new URL('/api/data', location.origin);201202 //read our state and pass it to the API as query params203 fetchURL.searchParams.set(204 'start',205 `${pagination.pageIndex * pagination.pageSize}`,206 );207 fetchURL.searchParams.set('size', `${pagination.pageSize}`);208 fetchURL.searchParams.set('filters', JSON.stringify(columnFilters ?? []));209 fetchURL.searchParams.set('globalFilter', globalFilter ?? '');210 fetchURL.searchParams.set('sorting', JSON.stringify(sorting ?? []));211212 //use whatever fetch library you want, fetch, axios, etc213 const response = await fetch(fetchURL.href);214 const json = (await response.json()) as UserApiResponse;215 return json;216 },217 placeholderData: keepPreviousData, //don't go to 0 rows when refetching or paginating to next page218 });219};220221//fetch more user info hook222const useFetchUserInfo = (223 params: { phoneNumber: string },224 options: { enabled: boolean },225) => {226 return useQuery<FullUserInfoApiResponse>({227 enabled: options.enabled, //only fetch when the detail panel is opened228 staleTime: 60 * 1000, //don't refetch for 60 seconds229 queryKey: ['user', params.phoneNumber], //give a unique key for this query for each user fetch230 queryFn: async () => {231 const fetchURL = new URL(232 `/api/moredata/${params.phoneNumber233 .replaceAll('-', '')234 .replaceAll('.', '')235 .replaceAll('(', '')236 .replaceAll(')', '')}`,237 location.origin,238 );239240 //use whatever fetch library you want, fetch, axios, etc241 const response = await fetch(fetchURL.href);242 const json = (await response.json()) as FullUserInfoApiResponse;243 return json;244 },245 });246};247
View Extra Storybook Examples