MRT logoMaterial React Table

On This Page

    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 ? ... : ...
    },
    }),
    });

    Demo

    Open StackblitzOpen Code SandboxOpen on GitHub
    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.

    Demo

    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.

    Demo

    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 { 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,
    };
    },
    });

    Demo

    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.

    Demo

    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