Replace the React Data Grid's toolbar, pagination and empty-state chrome with your own components through the slots prop.
Slots let you swap the grid’s built-in chrome — toolbar, pagination and
empty — for your own UI while the grid keeps driving state through the
headless table. Reach for a slot when the default control isn’t enough: a custom
toolbar with extra actions, a bespoke pager, or a richer empty state. For custom
cell content, use a column’s formatter or render
instead.
Custom slots
Replace the grid’s chrome — toolbar, pagination and empty — with your own
components. Each slot is a component that receives { table, labels } and renders
in place of the built-in module; it re-renders with the grid, so it always
reflects current state. This demo swaps the built-in pagination for a minimal
Previous/Next control driven through the headless table.
import { CDataGrid } from '@coreui/react-data-grid'
import type { CDataGridSlotProps } from '@coreui/react-data-grid'
import { useMemo } from 'react'
const firstNames = ['Alice', 'Bob', 'Carol', 'Dave', 'Eve', 'Frank', 'Grace', 'Heidi', 'Ivan', 'Judy']
const lastNames = ['Smith', 'Jones', 'Brown', 'Taylor', 'Wilson', 'Davies', 'Evans', 'Thomas']
const roles = ['admin', 'editor', 'viewer']
const Pagination = ({ table }: CDataGridSlotProps) => {
const { pageIndex } = table.getState().pagination
return (
<div className="d-flex gap-2 align-items-center mt-2">
<button
type="button"
className="btn btn-sm btn-outline-secondary"
disabled={!table.getCanPreviousPage()}
onClick={() => table.previousPage()}
>
Previous
</button>
<span className="text-body-secondary">
Page {pageIndex + 1} of {table.getPageCount()} · {table.getRowCount()} items
</span>
<button
type="button"
className="btn btn-sm btn-outline-secondary"
disabled={!table.getCanNextPage()}
onClick={() => table.nextPage()}
>
Next
</button>
</div>
)
}
export const DataGridSlotsExample = () => {
const items = useMemo(
() =>
Array.from({ length: 1000 }, (_, i) => {
const name = `${firstNames[i % firstNames.length]} ${lastNames[i % lastNames.length]}`
return {
id: i + 1,
name,
email: `${name.toLowerCase().replace(' ', '.')}${i}@example.com`,
role: roles[i % roles.length]
}
}),
[]
)
return (
<CDataGrid
columns={[
{ key: 'id', label: '#', width: 90 },
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email', style: { width: '30%' } },
{ key: 'role', label: 'Role', width: 110 }
]}
items={items}
itemKey={item => String(item.id)}
pagination={{ pageSize: 10 }}
slots={{ pagination: Pagination }}
/>
)
} import { CDataGrid } from '@coreui/react-data-grid'
import type { CDataGridSlotProps } from '@coreui/react-data-grid'
import { useMemo } from 'react'
const firstNames = ['Alice', 'Bob', 'Carol', 'Dave', 'Eve', 'Frank', 'Grace', 'Heidi', 'Ivan', 'Judy']
const lastNames = ['Smith', 'Jones', 'Brown', 'Taylor', 'Wilson', 'Davies', 'Evans', 'Thomas']
const roles = ['admin', 'editor', 'viewer']
const Pagination = ({ table }: CDataGridSlotProps) => {
const { pageIndex } = table.getState().pagination
return (
<div className="d-flex gap-2 align-items-center mt-2">
<button
type="button"
className="btn btn-sm btn-outline-secondary"
disabled={!table.getCanPreviousPage()}
onClick={() => table.previousPage()}
>
Previous
</button>
<span className="text-body-secondary">
Page {pageIndex + 1} of {table.getPageCount()} · {table.getRowCount()} items
</span>
<button
type="button"
className="btn btn-sm btn-outline-secondary"
disabled={!table.getCanNextPage()}
onClick={() => table.nextPage()}
>
Next
</button>
</div>
)
}
export const DataGridSlotsExample = () => {
const items = useMemo(
() =>
Array.from({ length: 1000 }, (_, i) => {
const name = `${firstNames[i % firstNames.length]} ${lastNames[i % lastNames.length]}`
return {
id: i + 1,
name,
email: `${name.toLowerCase().replace(' ', '.')}${i}@example.com`,
role: roles[i % roles.length]
}
}),
[]
)
return (
<CDataGrid
columns={[
{ key: 'id', label: '#', width: 90 },
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email', style: { width: '30%' } },
{ key: 'role', label: 'Role', width: 110 }
]}
items={items}
itemKey={item => String(item.id)}
pagination={{ pageSize: 10 }}
slots={{ pagination: Pagination }}
/>
)
} With pagination={{ position: 'both' }} the slot component renders once per
position. A custom toolbar that hosts its own global search still needs
globalFilter for the query to reach the grid. The empty slot also renders
when a server load fails; use the onDataError callback to tell the two apart.
Slot contract
| Slot | Replaces | Signature |
|---|---|---|
toolbar | The toolbar above the grid | ComponentType<{ table, labels }> |
pagination | The pagination bar | same |
empty | The no-rows / load-error state | same |
table— the headlesstableinstance; read current state and call its methods to drive the grid.labels— the resolved localization strings.- The component re-renders on every grid render, so it always reflects current state — no manual update or teardown hooks needed.