React Data Grid Slots & Custom Rendering

Slots & custom rendering

Replace the React Data Grid's toolbar, pagination and empty-state chrome with your own components through the slots prop.

On this page

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

SlotReplacesSignature
toolbarThe toolbar above the gridComponentType<{ table, labels }>
paginationThe pagination barsame
emptyThe no-rows / load-error statesame
  • table — the headless table instance; 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.