Skip to content

DataTable

Feature-rich table component with:

  • Sortable headers with tooltips
  • Support for custom renderers and field components
  • Row click navigation (show/edit) logic
  • Expandable rows
  • Row selection & bulk actions toolbar
  • Column visibility & reordering (via ColumnsButton)
  • Pagination (via ListPagination)
  • Conditional row and cell classes
  • Sticky Headers

It leverages shadcn/ui’s Table component for the base markup and styling.

Use <DataTable> inside a ListContext (e.g., as a descendent of <List> or <ReferenceManyField>). Define the table columns with its children using <DataTable.Col> components:

import { List, DataTable, ReferenceField, EditButton } from '@/components/admin';
export const PostList = () => (
<List>
<DataTable>
<DataTable.Col source="id" />
<DataTable.Col label="User">
<ReferenceField source="user_id" reference="users" />
</DataTable.Col>
<DataTable.Col source="title" />
<DataTable.Col>
<EditButton />
</DataTable.Col>
</DataTable>
</List>
);

Each <DataTable.Col> child defines how to label the column header, either via the label prop, or by humanizing the source prop.

<DataTable.Col> also defines where to get the value for each cell in that column (either via source, a render prop, or a child component). <DataTable> renders each row in a RecordContext, so any Field component can be used inside <DataTable.Col>.

It also accepts additional props to configure the behavior of that specific column, such as sorting, styling, etc.

PropRequiredTypeDefaultDescription
childrenRequiredReactNode-Column definitions (DataTable.Col / custom)
bulkActionButtonsOptionalReactNode | falseBulk Delete and ExportCustom bulk action buttons or disable with false
bulkActionsToolbarOptionalReactNode-Full custom toolbar (overrides default)
classNameOptionalstring-Wrapper classes
emptyOptionalElement<Empty>The component to render when the list is empty.
expandOptionalElement-The component rendering the expand panel for each row.
expandSingleOptionalBooleanfalseWhether to allow only one expanded row at a time.
hiddenColumnsOptionalArray[]The list of columns to hide by default (to be used with ColumnsButton) .
isRowExpandableOptionalFunction() => trueA function that returns whether a row is expandable.
isRowSelectableOptionalFunction() => trueA function that returns whether a row is selectable.
rowClassNameOptional(record) => string-Dynamic row classes
rowClickOptionalmixedshowThe action to trigger when the user clicks on a row.
storeKeyOptionalstring<resource>.datatablePersistence key for column state

For non-numeric values, use <DataTable.Col>. It lets you define how the data renders in 4 different ways:

  • By passing a source prop and no child.
<DataTable.Col source="firstName" />
  • By passing child elements (e.g. <ReferenceField>, <DateField>, etc.).
<DataTable.Col source="lastName">
<TextField source="firstName" />{" "}<TextField source="lastName" />
</DataTable.Col>
  • By using the field prop to specify a field component.
<DataTable.Col source="createdAt" field={DateField} />
  • By passing a render prop to define a custom rendering function.
<DataTable.Col
label="Name"
source="lastName"
render={(record) => `${record.firstName} ${record.lastName}`}
/>

Even when using children, field, or render, you can still pass a source prop to define the column label and enable sorting on that column.

<DataTable.Col> accepts the following additional props:

PropRequiredTypeDescription
headerClassNameOptionalstringExtra header cell classes
cellClassNameOptionalstringExtra body cell classes
conditionalClassNameOptional(record) => stringAdds per-row class
disableSortOptionalbooleanDisable sorting on this column
sortByOrderOptional"ASC"|"DESC"Initial sort order when first clicked
labelOptionalReactNodeHeader label (i18n key or node)

For numeric values, prefer <DataTable.NumberCol>. It is right-aligned and uses <NumberField> to format the value. You can pass an options prop to configure the number format.

<DataTable.NumberCol source="amount" options={{ style: 'currency', currency: 'USD' }} />

<DataTable.NumberCol> accepts the following props, in addition to those of <DataTable.Col>:

PropTypeDescription
localesstring | string[]Intl locales
optionsIntl.NumberFormatOptionsFormat options

Bulk action buttons appear when users select one or several rows. Clicking on a bulk action button affects all the selected records. This is useful for actions like mass deletion or mass edition.

You can disable this feature by setting the bulkActionButtons prop to false:

import { DataTable, List } from '@/components/admin';
export const PostList = () => (
<List>
<DataTable bulkActionButtons={false}>
...
</DataTable>
</List>
);

By default, all DataTables have a two bulk action buttons: bulk export and bulk delete. You can add other bulk action buttons by passing a custom element as the <DataTable bulkActionButtons> prop:

import { List, DataTable, BulkDeleteButton, BulkExportButton } from '@/components/admin';
const PostBulkActionButtons = () => (
<>
<ResetViewsButton />
<BulkDeleteButton />
<BulkExportButton />
</>
);
export const PostList = () => (
<List>
<DataTable bulkActionButtons={<PostBulkActionButtons />}>
...
</DataTable>
</List>
);

Shadcn Admin Kit provides two bulk action buttons that you can use in data tables:

You can write a custom bulk action button components using the useListContext hook to get the following data and callbacks:

  • selectedIds: the identifiers of the currently selected items.
  • onUnselectItems: a callback to empty the selection.
  • resource: the currently displayed resource (e.g., posts, comments, etc.)
  • filterValues: the filter values. This can be useful if you want to apply your action on all items matching the filter.

Here is an example leveraging the useUpdateMany hook, which sets the views property of all posts to 0:

import {
useListContext,
useUpdateMany,
useRefresh,
useNotify,
useUnselectAll,
} from 'ra-core';
import { Button } from '@/components/admin';
import { EyeOff } from 'lucide-react';
const ResetViewsButton = () => {
const { selectedIds } = useListContext();
const refresh = useRefresh();
const notify = useNotify();
const unselectAll = useUnselectAll('posts');
const [updateMany, { isPending }] = useUpdateMany();
const handleClick = () => {
updateMany(
'posts',
{ ids: selectedIds, data: { views: 0 } },
{
onSuccess: () => {
notify('Posts updated', { undoable: true });
unselectAll();
},
onError: () => {
notify('Error: posts not updated', { type: 'error' });
refresh();
},
mutationMode: 'undoable',
}
);
}
return (
<Button onClick={handleClick} disabled={isPending}>
<EyeOff /> Reset views
</Button>
);
};

The column headers are buttons that allow users to change the list sort field and order. This feature requires no configuration and works out of the box.

It is possible to disable sorting for a specific <DataTable.Col> by passing a sortable property set to false:

export const PostList = () => (
<List>
<DataTable>
<DataTable.Col source="id" sortable={false} />
<DataTable.Col source="title" />
<DataTable.Col source="body" />
</DataTable>
</List>
);

By default, a column is sorted by the <DataTable.Col source> property.

For example, the following column displays the full name of a contact and is sortable by their last name:

<DataTable.Col
label="Name"
source="lastName"
render={record => `${record.firstName} ${record.lastName}`}
/>

An action column should not be sortable, so you don’t need to specify a source:

<DataTable.Col>
<EditButton />
<DeleteButton />
</DataTable.Col>

You can also use a different source for the column and its child. This is very useful for reference fields, where users expect the column to be sortable by the reference (e.g., author.name) rather than the foreign key (e.g., author_id):

<DataTable.Col source="authors(name)" label="Author" >
<ReferenceField source="author_id" reference="authors" />
</DataTable.Col>

By default, when the user clicks on a column header, the list becomes sorted in ascending order. You change this behavior by setting the sortByOrder prop to "DESC" in a <DataTable.Col> element:

<DataTable.Col source="published_at" sortByOrder="DESC"/>

You can let end users customize the fields displayed in the <DataTable> by using the <ColumnsButton> in the <List actions>. When users click on this button, they can show / hide columns and reorder them.

import { ColumnsButton, List, DataTable } from '@/components/admin';
const PostListActions = () => (
<div className="flex items-center gap-2">
<ColumnsButton />
</div>
)
const PostList = () => (
<List actions={<PostListActions />}>
<DataTable>
<DataTable.Col source="id" />
<DataTable.Col source="title" />
<DataTable.Col source="author" />
<DataTable.Col source="year" />
</DataTable>
</List>
);

By default, <DataTable> renders all <DataTable.Col> children. But you can also omit some of them by setting the hiddenColumns prop. Hidden columns are still displayed in the <ColumnsButton> dialog, so users can show them again.

const PostList = () => (
<List actions={<PostListActions />}>
<DataTable hiddenColumns={['id', 'author']}>
<DataTable.Col source="id" />
<DataTable.Col source="title" />
<DataTable.Col source="author" />
<DataTable.Col source="year" />
</DataTable>
</List>
);

If you render more than one <DataTable> in the same page, you must pass a unique storeKey prop to each one:

const PostList = () => (
<List>
<DataTable storeKey="posts.DataTable">
...
</DataTable>
</List>
);

If you include a <ColumnsButton> in a page that has more than one <DataTable>, you have to link the two components by giving them the same storeKey:

const PostListActions = () => (
<TopToolbar>
<ColumnsButton storeKey="posts.DataTable" />
</TopToolbar>
);
const PostList = () => (
<List actions={<PostListActions />}>
<DataTable storeKey="posts.DataTable">
...
</DataTable>
</List>
);

You can change the style of a row based on the record values by using the rowClassName prop. This prop is a function that takes the current record as an argument and returns a string.

import { DataTable, List } from '@/components/admin';
export const PostList = () => (
<List>
<DataTable
rowClassName={(record) =>
record.is_published ? 'bg-white' : 'bg-gray-50'
}
>
...
</DataTable>
</List>
);

You can also change the style of a specific cell based on the record values by using the conditionalClassName prop of <DataTable.Col>. This prop is a function that takes the current record as an argument and returns a string.

import { DataTable, List } from '@/components/admin';
export const PostList = () => (
<List>
<DataTable>
<DataTable.Col source="id" />
<DataTable.Col source="title" />
<DataTable.Col
source="views"
conditionalClassName={(record) =>
record.views > 1000 ? 'font-bold' : ''
}
/>
</DataTable>
</List>
);

If you need to hide some columns based on a set of permissions, wrap these columns with <CanAccess>.

import { CanAccess } from 'ra-core';
const ProductList = () => (
<List>
<DataTable>
<CanAccess action="read" resource="products.thumbnail">
<DataTable.Col source="thumbnail" field={ImageField} />
</CanAccess>
<CanAccess action="read" resource="products.reference">
<DataTable.Col source="reference" />
</CanAccess>
<CanAccess action="read" resource="products.category_id">
<DataTable.Col source="category_id">
<ReferenceField source="category_id" reference="categories" />
</DataTable.Col>
</CanAccess>
<CanAccess action="read" resource="products.width">
<DataTable.NumberCol source="width" />
</CanAccess>
<CanAccess action="read" resource="products.height">
<DataTable.NumberCol source="height" />
</CanAccess>
<CanAccess action="read" resource="products.price">
<DataTable.NumberCol source="price" />
</CanAccess>
<CanAccess action="read" resource="products.description">
<DataTable.Col source="description" />
</CanAccess>
<CanAccess action="read" resource="products.stock">
<DataTable.NumberCol source="stock" />
</CanAccess>
<CanAccess action="read" resource="products.sales">
<DataTable.NumberCol source="sales" />
</CanAccess>
</DataTable>
</List>
);

<DataTable.Col> and <DataTable.NumberCol> are generic components, You can pass a type parameter to get hints for the source prop and type safety for the record argument of the render and rowSx functions.

The most convenient way to benefit from this capability is to alias column components for your resource:

import { List, DataTable, ReferenceField } from '@/components/admin';
import { type Review } from '../types';
const Column = DataTable.Col<Review>;
const ReviewList = () => (
<List>
<DataTable>
<Column source="date" field={DateField} />
<Column source="customer_id">
<ReferenceField source="customer_id" reference="customers"/>
</Column>
<Column source="product_id">
<ReferenceField source="product_id" reference="products" />
</Column>
<Column source="rating" field={StarRatingField} />
<Column
source="comment"
render={record => record.comment.substr(0, 10) + '...'}
/>
<Column source="status" />
</DataTable>
</List>
);

<DataTable> is also a generic component. You can pass a type parameter to get autocompletion and type safety for its props.

import { List, DataTable } from '@/components/admin';
import { type Review } from '../types';
const ReviewList = () => (
<List>
<DataTable<Review>
// TypeScript knows that record type is Review
rowSx={record => ({
backgroundColor: record.status === 'approved' ? 'green' : 'red',
})}
>
...
</DataTable>
</List>
);