Theme

Examples › Expanding rows

The rowExpansion property allows you to define the “row expansion” behavior of the DataTable.

Basic usage

In its most basic usage scenario, the feature only requires specifying the content to be lazily rendered when a row is expanded.
return (
<DataTable
withBorder
withColumnBorders
columns={[{ accessor: 'name' }, { accessor: 'city' }, { accessor: 'state' }]}
records={records}
rowExpansion={{
content: ({ record }) => (
<Stack className={classes.details} p="xs" spacing={6}>
<Group spacing={6}>
<Text className={classes.label}>Postal address:</Text>
<Text>
{record.streetAddress}, {record.city}, {record.state}
</Text>
</Group>
<Group spacing={6}>
<Text className={classes.label}>Mission statement:</Text>
<Text italic>{record.missionStatement}</Text>
</Group>
</Stack>
),
}}
/>
);
Click on a row to test the behavior:
Sipes IncTwin FallsMT
Runolfsdottir - CummerataMissouri CityKY
Johnston LLCHartfordKY
Crist and SonsAttleboroWV
Schmidt and SonsColliervilleAL
No records

Specifying collapse properties

Internally, the expanded content is rendered inside a Mantine Collapse component. You can customize the underlying Collapse component like so:
return (
<DataTable
// ...
rowExpansion={{
collapseProps: {
transitionDuration: 500,
animateOpacity: false,
transitionTimingFunction: 'ease-out',
},
// ...
}}
/>
);
Sipes IncTwin FallsMT
Runolfsdottir - CummerataMissouri CityKY
Johnston LLCHartfordKY
Crist and SonsAttleboroWV
Schmidt and SonsColliervilleAL
No records

Specifying which rows are initially expanded

You can specify which rows are initially expanded like so:
return (
<DataTable
withBorder
withColumnBorders
columns={[{ accessor: 'name' }, { accessor: 'city' }, { accessor: 'state' }]}
records={records}
rowExpansion={{
initiallyExpanded: (record) => record.name === 'Johnston LLC',
// ...
}}
/>
);
Sipes IncTwin FallsMT
Runolfsdottir - CummerataMissouri CityKY
Johnston LLCHartfordKY
Postal address:
230 Julie Lake, Hartford, KY
Mission statement:
Transition wireless initiatives.
Crist and SonsAttleboroWV
Schmidt and SonsColliervilleAL
No records

Allowing multiple rows to be expanded at once

By default, a single row can be expanded at a certain time. You can override the default behavior like so:
return (
<DataTable
withBorder
withColumnBorders
columns={[{ accessor: 'name' }, { accessor: 'city' }, { accessor: 'state' }]}
records={records}
rowExpansion={{
allowMultiple: true,
// ...
}}
/>
);
Sipes IncTwin FallsMT
Runolfsdottir - CummerataMissouri CityKY
Johnston LLCHartfordKY
Crist and SonsAttleboroWV
Schmidt and SonsColliervilleAL
No records

Always expand all rows

If you want all rows to be locked in their expanded state, just set the row expansion trigger property to always:
return (
<DataTable
withBorder
withColumnBorders
columns={[{ accessor: 'name' }, { accessor: 'city' }, { accessor: 'state' }]}
records={records}
rowExpansion={{
trigger: 'always',
// ...
}}
/>
);
Sipes IncTwin FallsMT
Postal address:
280 Rigoberto Divide, Twin Falls, MT
Mission statement:
Strategize magnetic vortals.
Runolfsdottir - CummerataMissouri CityKY
Postal address:
102 Konopelski Greens, Missouri City, KY
Mission statement:
Leverage one-to-one methodologies.
Johnston LLCHartfordKY
Postal address:
230 Julie Lake, Hartford, KY
Mission statement:
Transition wireless initiatives.
Crist and SonsAttleboroWV
Postal address:
3387 Blick Turnpike, Attleboro, WV
Mission statement:
Revolutionize out-of-the-box infomediaries.
Schmidt and SonsColliervilleAL
Postal address:
286 Leif Lock, Collierville, AL
Mission statement:
Optimize bricks-and-clicks eyeballs.
No records

Using collapse() function in row expansion content

Besides the current record, the content function also receives a collapse callback that could be used, for instance, in an inline editor like so:
const [companies, setCompanies] = useState(initialRecords);
return (
<DataTable
withBorder
withColumnBorders
columns={[{ accessor: 'name' }, { accessor: 'city' }, { accessor: 'state' }]}
records={companies}
rowExpansion={{
content: ({ record, collapse }) => (
<CompanyEditor
initialData={record}
onDone={(data) => {
const index = companies.findIndex((c) => c.id === data.id);
setCompanies([...companies.slice(0, index), data, ...companies.slice(index + 1)]);
collapse();
}}
onCancel={collapse}
/>
),
}}
/>
);
const useStyles = createStyles((theme) => ({
details: { background: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0] },
}));
type CompanyEditorProps = {
initialData: Company;
onDone: (data: Company) => void;
onCancel: () => void;
};
function CompanyEditor({ initialData, onDone, onCancel }: CompanyEditorProps) {
const [name, setName] = useState(initialData.name);
const [city, setCity] = useState(initialData.city);
const [state, setState] = useState(initialData.state);
const [streetAddress, setStreetAddress] = useState(initialData.streetAddress);
const [missionStatement, setMissionStatement] = useState(initialData.missionStatement);
const { classes } = useStyles();
return (
<Box className={classes.details} p="md">
<Grid>
<Grid.Col span={12} xs={6}>
<TextInput label="Name" size="xs" value={name} onChange={(e) => setName(e.currentTarget.value)} />
</Grid.Col>
{/* other fields... */}
<Grid.Col span={12}>
<Group position="center">
<Button variant="default" size="xs" leftIcon={<IconArrowBackUp size={16} />} onClick={() => onCancel()}>
Cancel
</Button>
<Button
size="xs"
leftIcon={<IconCheck size={16} />}
onClick={() =>
onDone({
...initialData,
name: name.trim(),
city: city.trim(),
state: state.trim(),
streetAddress: streetAddress.trim(),
missionStatement: missionStatement.trim(),
})
}
>
Save
</Button>
</Group>
</Grid.Col>
</Grid>
</Box>
);
}
Sipes IncTwin FallsMT
Runolfsdottir - CummerataMissouri CityKY
Johnston LLCHartfordKY
Crist and SonsAttleboroWV
Schmidt and SonsColliervilleAL
No records

Lazy-loading row expansion data

As mentioned above, the content function is lazily executed when a row is expanded to prevent creating unnecessary DOM elements.
If your row expansion content needs to show data that comes from outside the table records, you could exploit this behavior to lazy-load it only when a row is expanded:
return (
<DataTable
withBorder
withColumnBorders
columns={[{ accessor: 'name' }, { accessor: 'city' }, { accessor: 'state' }]}
records={records}
rowExpansion={{ content: ({ record }) => <CompanyDetails companyId={record.id} /> }}
/>
);
const useStyles = createStyles((theme) => ({
details: {
position: 'relative',
background: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0],
},
label: { width: 180 },
number: { width: 50 },
}));
function CompanyDetails({ companyId }: { companyId: string }) {
const isMounted = useIsMounted();
const [loading, setLoading] = useState(true);
const [numberOfDepartments, setNumberOfDepartments] = useState<number | null>(null);
const [numberOfEmployees, setNumberOfEmployees] = useState<number | null>(null);
useEffect(() => {
// simulate expensive async loading operation
(async () => {
setLoading(true);
const delay = { min: 800, max: 1200 };
const [departments, employees] = await Promise.all([
countCompanyDepartmentsAsync({ companyId, delay }),
countCompanyEmployeesAsync({ companyId, delay }),
]);
if (isMounted()) {
setNumberOfDepartments(departments);
setNumberOfEmployees(employees);
setLoading(false);
}
})();
}, [companyId, isMounted]);
const { classes } = useStyles();
return (
<Center className={classes.details} p="sm">
<Stack spacing={6}>
<LoadingOverlay visible={loading} />
<Group spacing={6}>
<Text className={classes.label}>Number of departments:</Text>
<Text className={classes.number} align="right">
{numberOfDepartments ?? 'loading...'}
</Text>
</Group>
<Group spacing={6}>
<Text className={classes.label}>Number of employees:</Text>
<Text className={classes.number} align="right">
{numberOfEmployees ?? 'loading...'}
</Text>
</Group>
</Stack>
</Center>
);
}
Sipes IncTwin FallsMT
Runolfsdottir - CummerataMissouri CityKY
Johnston LLCHartfordKY
Crist and SonsAttleboroWV
Schmidt and SonsColliervilleAL
No records

Controlled mode

You can control the row expansion feature by pointing the rowExpansion/expanded property to an object containing:
  • recordIds → an array containing the currently expanded record IDs
  • onRecordIdsChange → a callback function that gets called when the currently expanded records change
When using the row expansion feature in controlled mode, if you want to prevent the default behavior of toggling the expansion state on click, set the rowExpansion/trigger property to 'never'.
1Sipes IncTwin FallsMT
2Runolfsdottir - CummerataMissouri CityKY
3Johnston LLCHartfordKY
4Crist and SonsAttleboroWV
5Schmidt and SonsColliervilleAL
No records
const [expandedRecordIds, setExpandedRecordIds] = useState<string[]>([]);
const expandFirstAndThirdRow = () => {
setExpandedRecordIds([firstRowId, thirdRowId]);
};
const expandSecondAndFourthRow = () => {
setExpandedRecordIds([secondRowId, fourthRowId]);
};
const collapseAllRows = () => {
setExpandedRecordIds([]);
};
return (
<>
{/* buttons triggering the above callbacks... */}
<DataTable
mt="md"
withBorder
withColumnBorders
columns={[
{ accessor: 'number', title: '#', render: (_, index) => index + 1 },
{ accessor: 'name', width: '100%' },
{ accessor: 'city', ellipsis: true },
{ accessor: 'state' },
]}
records={records}
rowExpansion={{
// trigger: 'never', // 👈 uncomment this if you want to disable expanding/collapsing on click
allowMultiple: true,
expanded: {
recordIds: expandedRecordIds,
onRecordIdsChange: setExpandedRecordIds,
},
content: ({ record }) => (
// expansion content...
),
}}
/>
</>
);
Head over to the next example to see how you can abuse the row expansion feature to display nested tables.

Mantine DataTable is trusted by

MIT LicenseSponsor the author
Built by Ionut-Cristian Florescu and these awesome people.
Please sponsor the project if you find it useful.
GitHub StarsNPM Downloads