Replace the Data Grid's toolbar, pagination and empty-state chrome with your own markup through named scoped slots.
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 a cell-{key} slot
instead.
Custom slots
Replace the grid’s chrome — toolbar, pagination and empty — with your own
content. Each named slot receives { table, labels } as slot props 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.
<script setup lang="ts">
import { CDataGrid } from '@coreui/vue-data-grid'
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 items = 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]
}
})
const columns = [
{ key: 'id', label: '#', width: 90 },
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email', style: { width: '30%' } },
{ key: 'role', label: 'Role', width: 110 }
]
</script>
<template>
<CDataGrid
:columns="columns"
:items="items"
:item-key="(item) => String(item.id)"
:pagination="{ pageSize: 10 }"
>
<template #pagination="{ table }">
<div class="d-flex gap-2 align-items-center mt-2">
<button
type="button"
class="btn btn-sm btn-outline-secondary"
:disabled="!table.getCanPreviousPage()"
@click="table.previousPage()"
>
Previous
</button>
<span class="text-body-secondary">
Page {{ table.getState().pagination.pageIndex + 1 }} of {{ table.getPageCount() }} ·
{{ table.getRowCount() }} items
</span>
<button
type="button"
class="btn btn-sm btn-outline-secondary"
:disabled="!table.getCanNextPage()"
@click="table.nextPage()"
>
Next
</button>
</div>
</template>
</CDataGrid>
</template> With pagination.position: 'both' the slot renders once per position. A custom
toolbar that hosts its own global search still needs global-filter for the
query to reach the grid. The empty slot also renders when a server load fails;
use the data-error event to tell the two apart.
Slot contract
| Slot | Replaces | Slot props |
|---|---|---|
toolbar | The toolbar above the grid | { table, labels } |
pagination | The pagination bar | same |
empty | The no-rows / load-error state | same |
table— the headless table instance; read current state and call its setters to drive the grid.labels— the merged localization strings.
Slot content re-renders reactively with the grid, so there’s no manual update or teardown to manage.