Replace the Angular Data Grid's toolbar, pagination and empty-state chrome with your own markup through cDataGridSlot templates.
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 cDataGridCell template instead.
Custom slots
Replace the grid’s chrome — toolbar, pagination and empty — with your own
markup. Each slot is an <ng-template cDataGridSlot="..."> receiving the
headless table (implicit) and labels in its context 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 { Component } from '@angular/core'
import { DataGridComponent, DataGridSlotDirective } from '@coreui/angular-data-grid'
import type { DataGridColumn, DataGridItem } from '@coreui/angular-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']
@Component({
selector: 'docs-data-grid-slots-example',
imports: [DataGridComponent, DataGridSlotDirective],
template: `
<c-data-grid
[columns]="columns"
[items]="items"
[itemKey]="itemKey"
[pagination]="{ pageSize: 10 }"
>
<!-- Minimal Previous/Next pagination driven through the headless table -->
<ng-template cDataGridSlot="pagination" let-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>
</ng-template>
</c-data-grid>
`
})
export class DataGridSlotsExample {
readonly columns: DataGridColumn[] = [
{ key: 'id', label: '#', width: 90 },
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email', style: { width: '30%' } },
{ key: 'role', label: 'Role', width: 110 }
]
readonly items: DataGridItem[] = 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]
}
})
readonly itemKey = (item: DataGridItem) => String(item.id)
} With pagination.position: 'both' the same template renders once per position.
A custom toolbar that hosts its own global search still needs
[globalFilter]="true" for the query to reach the grid. The empty slot also
renders when a server load fails; use the dataError output to tell the two
apart.
Slot contract
| Slot | Replaces | Template |
|---|---|---|
toolbar | The toolbar above the grid | <ng-template cDataGridSlot="toolbar"> |
pagination | The pagination bar | <ng-template cDataGridSlot="pagination"> |
empty | The no-rows / load-error state | <ng-template cDataGridSlot="empty"> |
$implicit— the headlesstable; read current state from it (e.g.let-table).labels— the merged UI strings, for translatable custom chrome.- The template re-renders on every grid state change, so it never goes stale; Angular’s lifecycle handles cleanup.