# CoreUI Data Grid JavaScript documentation
> High-performance data grid for CoreUI — 100,000 rows with sorting, filtering, selection and pagination.
---
# CoreUI Data Grid
> High-performance data grid — 100,000 rows with sorting, filtering, selection, pagination, server-side data, column resizing, pinning, ordering and full theming, around 63 KB gzipped.
CoreUI Data Grid is a high-performance grid for displaying and interacting with
large tabular datasets. It renders 100,000 rows in the browser with sorting,
filtering, selection and pagination, and hands off to your API when the data
outgrows the browser's memory.
## Why Data Grid
- **Fast by default.** Row [virtualization](https://coreui.io/data-grid/docs/features/virtualization/) keeps only
the visible window in the DOM, so scrolling stays smooth at 100k rows.
- **Headless core, styled shell.** A proven headless table engine and row
virtualization under a CoreUI-themed UI. Drop to the
[headless table](https://coreui.io/data-grid/docs/api/headless/) any time.
- **Complete feature set.** Sorting, [filtering](https://coreui.io/data-grid/docs/features/filtering/),
[selection](https://coreui.io/data-grid/docs/features/row-selection/), [pagination](https://coreui.io/data-grid/docs/features/pagination/),
[server-side data](https://coreui.io/data-grid/docs/features/server-side-data/), column
[sizing](https://coreui.io/data-grid/docs/columns/sizing/), [pinning](https://coreui.io/data-grid/docs/columns/pinning/),
[ordering & visibility](https://coreui.io/data-grid/docs/columns/ordering-visibility/), a
[column menu](https://coreui.io/data-grid/docs/columns/menu/) and [CSV export](https://coreui.io/data-grid/docs/features/csv-export/).
- **Themeable.** Every knob is a `--cui-data-grid-*` CSS variable resolving
through CoreUI semantic tokens, so light/dark theming works with no extra CSS.
See [Styling & theming](https://coreui.io/data-grid/docs/customization/styling/).
- **Small.** Around 63 KB gzipped for the vanilla build — 52 KB with the [lite build](https://coreui.io/data-grid/docs/customization/feature-sets/) when sorting, filtering and pagination are all you need.
## How it's built
The grid is a thin, styled layer over a headless table engine (state, sorting,
filtering, pagination, pinning, ordering, visibility) and row virtualization
(windowed rendering). Options are named after the features they control, events
are verbs, and event payloads carry the grid's own state — a small, predictable
API.
## Get started
1. [Install](https://coreui.io/data-grid/docs/getting-started/installation/) the package.
2. Follow the [Quickstart](https://coreui.io/data-grid/docs/getting-started/quickstart/) to render your first
grid.
3. Browse the [feature matrix](https://coreui.io/data-grid/docs/getting-started/features/) to see what's
available.
## Packages
| Package | Framework | Docs |
| ------- | --------- | ---- |
| `@coreui/data-grid` | Vanilla JavaScript | [coreui.io/data-grid/docs](https://coreui.io/data-grid/docs/) |
| `@coreui/react-data-grid` | React | [coreui.io/data-grid/react/docs](https://coreui.io/data-grid/react/docs/) |
| `@coreui/vue-data-grid` | Vue | [coreui.io/data-grid/vue/docs](https://coreui.io/data-grid/vue/docs/) |
| `@coreui/angular-data-grid` | Angular | [coreui.io/data-grid/angular/docs](https://coreui.io/data-grid/angular/docs/) |
---
# Data Grid Installation
> Install CoreUI Data Grid via npm and load its stylesheet, or drop in the UMD bundle.
## npm
```sh
npm install @coreui/data-grid
```
Data Grid ships its own stylesheet. It builds on the `.table`, `.btn` and form
styles from `@coreui/coreui` or `@coreui/coreui-pro` (≥ 5) when one of them is on
the page, and ships its own copy of them when it is not — see
[Stylesheet](#stylesheet) below.
```js
import { DataGrid } from '@coreui/data-grid'
import '@coreui/data-grid/dist/css/data-grid.css'
```
## UMD bundle
Without a bundler, include the script and stylesheet and use the global
`coreui.DataGrid`:
```html
```
## Stylesheet
Two builds ship in the package — load exactly one:
| File | Load it when |
| --- | --- |
| `data-grid.css` | `@coreui/coreui` or `@coreui/coreui-pro` (≥ 5) is already on the page. |
| `data-grid.standalone.css` | It is not. Adds ~2.7 kB gzip. |
`data-grid.standalone.css` carries the slice of CoreUI the grid's markup relies
on — `.table`, `.btn`, the form controls, `.pagination`, `.spinner-border` and
`.visually-hidden`. Those rules are scoped to `.data-grid` and emitted into a
`data-grid-base` cascade layer, so they never restyle the rest of your page, and
an unlayered CoreUI stylesheet always overrides them.
The grid's CSS defines `--cui-data-grid-*` custom properties that resolve through
CoreUI's semantic variables, so it inherits your theme (including
`data-coreui-theme="dark"`) automatically. See
[Styling & theming](https://coreui.io/data-grid/docs/customization/styling/) for the full token reference.
Next: the [Quickstart](https://coreui.io/data-grid/docs/getting-started/quickstart/).
---
# Data Grid Quickstart
> Render your first CoreUI Data Grid in a few lines — columns, data, a stable row key, then your first feature.
This guide builds a working grid from scratch. It assumes you've
[installed](https://coreui.io/data-grid/docs/getting-started/installation/) `@coreui/data-grid` and loaded its
stylesheet.
## 1. A container
The grid renders into any element:
```html
```
## 2. Columns and data
Define columns by `key` (the property to read from each item) and pass your
`items`:
```js
import { DataGrid } from '@coreui/data-grid'
import '@coreui/data-grid/dist/css/data-grid.css'
const items = [
{ id: 1, name: 'Alice', role: 'admin' },
{ id: 2, name: 'Bob', role: 'editor' },
{ id: 3, name: 'Carol', role: 'viewer' },
]
const grid = new DataGrid(document.getElementById('grid'), {
columns: [
{ key: 'name', label: 'Name' },
{ key: 'role', label: 'Role' },
],
items,
itemKey: (item) => String(item.id),
})
```
`itemKey` returns a stable id per row. It's optional, but
[selection](https://coreui.io/data-grid/docs/features/row-selection/) needs it to survive sorting and filtering —
set it up front.
## 3. Turn on a feature
Every feature is a single option. Add filtering and selection:
```js
const grid = new DataGrid(element, {
columns,
items,
itemKey: (item) => String(item.id),
columnFilters: true, // per-column filter row
rowSelection: true, // checkbox column with select-all
})
```
Sorting is on by default. From here, explore the
[feature matrix](https://coreui.io/data-grid/docs/getting-started/features/) or jump to any feature page.
## 4. React to changes
The grid emits namespaced [events](https://coreui.io/data-grid/docs/api/events/) with structured state:
```js
element.addEventListener('selectionChange.coreui.data-grid', (event) => {
console.log(event.selectedItems)
})
```
## What's next
- Handle large or remote data with [server-side data](https://coreui.io/data-grid/docs/features/server-side-data/).
- Customize cells with a column [`formatter` or `render`](https://coreui.io/data-grid/docs/columns/overview/).
- Replace built-in chrome with [slots](https://coreui.io/data-grid/docs/features/slots/) or drive the
[headless table](https://coreui.io/data-grid/docs/api/headless/) directly.
---
# Data Grid Features
> A capability matrix of everything CoreUI Data Grid does today, with the option that turns each feature on and a link to its docs.
Everything the Data Grid does today, the option that enables it, and where to
read more. Features not listed here are on the [roadmap](https://coreui.io/data-grid/docs/resources/roadmap/).
## Data & rendering
| Feature | Option | Docs |
| --- | --- | --- |
| Row virtualization | `virtualization` (on by default) | [Virtualization](https://coreui.io/data-grid/docs/features/virtualization/) |
| Pagination | `pagination` | [Pagination](https://coreui.io/data-grid/docs/features/pagination/) |
| Server-side data | `dataProvider` | [Server-side data](https://coreui.io/data-grid/docs/features/server-side-data/) |
| Row selection | `rowSelection` | [Row selection](https://coreui.io/data-grid/docs/features/row-selection/) |
| Infinite scroll | `infiniteScroll` | [Infinite scroll](https://coreui.io/data-grid/docs/features/infinite-scroll/) |
| Auto row height | `autoRowHeight` | [Auto row height](https://coreui.io/data-grid/docs/features/virtualization/#auto-row-height) |
| Row reordering | `rowOrder` | [Row reordering](https://coreui.io/data-grid/docs/features/row-reordering/) |
## Sorting & filtering
| Feature | Option | Docs |
| --- | --- | --- |
| Column sorting (multi-column) | `sorting` (on by default) | [Sorting](https://coreui.io/data-grid/docs/features/sorting/) |
| Per-column filter row | `columnFilters` | [Filtering](https://coreui.io/data-grid/docs/features/filtering/) |
| Global search | `globalFilter` | [Filtering](https://coreui.io/data-grid/docs/features/filtering/) |
| Custom filter UI / predicate | `filter`, `filterFn` (per column) | [Filtering](https://coreui.io/data-grid/docs/features/filtering/) |
## Columns
| Feature | Option | Docs |
| --- | --- | --- |
| Custom cell formatting / rendering | `formatter`, `render` (per column) | [Columns overview](https://coreui.io/data-grid/docs/columns/overview/) |
| Column resizing | `columnSizing` | [Column sizing](https://coreui.io/data-grid/docs/columns/sizing/) |
| Column pinning | `columnPinning` | [Column pinning](https://coreui.io/data-grid/docs/columns/pinning/) |
| Column ordering (drag & drop) | `columnOrder` | [Ordering & visibility](https://coreui.io/data-grid/docs/columns/ordering-visibility/) |
| Column visibility | `columnVisibility` | [Ordering & visibility](https://coreui.io/data-grid/docs/columns/ordering-visibility/) |
| Column header menu | `columnMenu` | [Column menu](https://coreui.io/data-grid/docs/columns/menu/) |
## Interaction
| Feature | Option | Docs |
| --- | --- | --- |
| Keyboard navigation (ARIA grid) | `cellNavigation` | [Keyboard navigation](https://coreui.io/data-grid/docs/features/keyboard-navigation/) |
| Cell selection & clipboard copy | `cellSelection` | [Cell selection](https://coreui.io/data-grid/docs/features/cell-selection/) |
| Inline editing | `editing` | [Inline editing](https://coreui.io/data-grid/docs/features/editing/) |
| Undo & redo | `history` | [Undo & redo](https://coreui.io/data-grid/docs/features/history/) |
| Built-in toolbar | `toolbar` | [Toolbar](https://coreui.io/data-grid/docs/features/toolbar/) |
## Customization & output
| Feature | Option / API | Docs |
| --- | --- | --- |
| Custom toolbar / pagination / empty state | `slots` | [Slots](https://coreui.io/data-grid/docs/features/slots/) |
| CSV export | `getCsv()`, `downloadCsv()` | [CSV export](https://coreui.io/data-grid/docs/features/csv-export/) |
| Excel export (.xlsx) | `toolbar.export.exporter` | [Excel export](https://coreui.io/data-grid/docs/features/excel-export/) |
| Print | `toolbar.print`, `print()` | [Print](https://coreui.io/data-grid/docs/features/print/) |
| Save & restore state | `stateKey` | [Save & restore state](https://coreui.io/data-grid/docs/features/state/) |
| Feature sets (smaller bundle) | `features` | [Feature sets](https://coreui.io/data-grid/docs/customization/feature-sets/) |
| Theming (CSS variables) | `--cui-data-grid-*` | [Styling & theming](https://coreui.io/data-grid/docs/customization/styling/) |
| Localization (i18n) | `labels` | [Localization](https://coreui.io/data-grid/docs/customization/localization/) |
| Headless escape hatch | `grid.table` | [Headless table](https://coreui.io/data-grid/docs/api/headless/) |
---
# Data Grid Browsers and Devices
> The browsers and devices CoreUI Data Grid supports, the floor it targets, and the browserslist configuration that drives its CSS prefixes and bundle syntax.
## Supported browsers
The Data Grid targets the **latest stable releases** of every major browser, with
a floor of **Chrome 123, Edge 123, Firefox 129, Safari 17.5 and iOS 17.5**.
Measured with `npx browserslist --coverage`, that range covers **86.47%** of
global usage.
Browsers built on the same engines (Chromium, Gecko, WebKit) are not listed
separately but behave the same, as long as their engine is at or above those
versions. Proxy browsers that render server-side — Opera Mini, UC Browser Mini,
QQ Browser, KaiOS — are excluded outright and are not supported.
### What sets the floor
The floor is the CoreUI PRO baseline, shared with the component library the grid
sits next to, not a limit the grid's own code imposes. Its own requirements are
lower:
| What the grid uses | Available from |
| --- | --- |
| `inset-inline-start` / `inset-inline-end` (sticky offsets for pinned columns) | Chrome 87, Firefox 63, Safari 14.1 |
| `:is()` | Chrome 88, Firefox 78, Safari 14 |
| Private class fields, `??`, `?.`, `??=` in the shipped bundle | Chrome 85, Firefox 90, Safari 14.1 |
The grid would therefore run on engines older than the floor, but only the range
above is tested — and sharing the baseline means a page that mixes the grid with
CoreUI PRO components has a single set of supported engines, not two.
## The browserslist configuration
One file drives both the CSS prefixes Autoprefixer emits and the syntax level
[rolldown](https://rolldown.rs/) lowers the bundle to — including the bundled
`@tanstack/table-core` and `@tanstack/virtual-core`:
```text
last 2 major versions
not dead
unreleased versions
Chrome >= 123
Edge >= 123
Firefox >= 129
iOS >= 17.5
Safari >= 17.5
not and_uc > 0
not and_qq > 0
not kaios > 0
not op_mini all
```
Raising the floor is what keeps the bundle small: at the previous Chrome 60 /
Safari 12 floor the same code needed 57,477 B gzip against 54,742 B (measured
at the rolldown migration) — a 2.7 kB saving from the floor alone — and
Autoprefixer dropped every prefix but `-webkit-user-select`.
### Mobile devices
| | Chrome | Firefox | Safari | Android Browser & WebView |
| --- | --- | --- | --- | --- |
| **Android** | Supported | Supported | — | Supported |
| **iOS** | Supported | Supported | Supported | — |
### Desktop browsers
| | Chrome | Firefox | Microsoft Edge | Opera | Safari |
| --- | --- | --- | --- | --- | --- |
| **Mac** | Supported | Supported | Supported | Supported | Supported |
| **Windows** | Supported | Supported | Supported | Supported | — |
| **Linux** | Supported | Supported | — | Supported | — |
---
# LLMs.txt
> LLM-optimized documentation endpoints for CoreUI Data Grid — llms.txt, llms-full.txt, and a Markdown version of every page.
## Introduction
[llms.txt](https://llmstxt.org) is an emerging standard that helps AI models understand and navigate documentation. The CoreUI Data Grid docs expose three LLM-friendly endpoints so assistants can retrieve accurate, up-to-date content straight from the source.
For a richer, tool-based integration, see [MCP Server](https://coreui.io/data-grid/docs/ai-tools/mcp/).
## /llms.txt
A structured index of the documentation — every page as a titled, described link, grouped by section. It gives an LLM a compact map of what exists and where.
[Open llms.txt](https://coreui.io/data-grid/docs/llms.txt)
## /llms-full.txt
The entire documentation concatenated into a single Markdown file, so a model can ingest the whole set in one request.
[Open llms-full.txt](https://coreui.io/data-grid/docs/llms-full.txt)
## Markdown version of any page
Append `.md` to any documentation page URL to get its clean Markdown version, without the site chrome.
For example: [/data-grid/docs/features/sorting.md](https://coreui.io/data-grid/docs/features/sorting.md)
---
# MCP Server
> Bring the CoreUI Data Grid documentation into your AI coding assistant with the @coreui/docs-mcp Model Context Protocol server.
## Introduction
[Model Context Protocol (MCP)](https://modelcontextprotocol.io) is an open standard that lets AI assistants connect to external tools and data sources. The **`@coreui/docs-mcp`** server gives your assistant direct access to the official CoreUI documentation, so it answers from the current docs instead of relying on stale training data.
Point it at the CoreUI Data Grid docs with the `--base-path` option shown below. The `bootstrap` key selects the vanilla (JavaScript) edition.
It provides:
- **Documentation pages** — getting started, features, columns, and API reference.
- **Live content** — read on demand from `coreui.io`, always matching the latest release.
- **Structured API** — options, events, and methods for the grid.
The server runs locally over stdio via `npx` — no global install required.
## Installation
### Claude Code
Add the server with the CLI, then start a new session and run `/mcp` to verify the connection:
```bash
claude mcp add coreui-data-grid -s user -- npx -y @coreui/docs-mcp --framework bootstrap --base-path /data-grid/docs
```
### Cursor
Create `.cursor/mcp.json` in your project (or `~/.cursor/mcp.json` for global configuration):
```json
{
"mcpServers": {
"coreui-data-grid": {
"command": "npx",
"args": ["-y", "@coreui/docs-mcp", "--framework", "bootstrap", "--base-path", "/data-grid/docs"]
}
}
}
```
### VS Code
Create `.vscode/mcp.json` in your project. Note that VS Code uses the `servers` key:
```json
{
"servers": {
"coreui-data-grid": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@coreui/docs-mcp", "--framework", "bootstrap", "--base-path", "/data-grid/docs"]
}
}
}
```
### Windsurf
Edit `~/.codeium/windsurf/mcp_config.json`:
```json
{
"mcpServers": {
"coreui-data-grid": {
"command": "npx",
"args": ["-y", "@coreui/docs-mcp", "--framework", "bootstrap", "--base-path", "/data-grid/docs"]
}
}
}
```
### Claude Desktop
Edit `claude_desktop_config.json` (Settings → Developer → Edit Config):
```json
{
"mcpServers": {
"coreui-data-grid": {
"command": "npx",
"args": ["-y", "@coreui/docs-mcp", "--framework", "bootstrap", "--base-path", "/data-grid/docs"]
}
}
}
```
### OpenAI Codex
Add it with the CLI, or edit `~/.codex/config.toml` directly:
```bash
codex mcp add coreui-data-grid -- npx -y @coreui/docs-mcp --framework bootstrap --base-path /data-grid/docs
```
```toml
[mcp_servers.coreui-data-grid]
command = "npx"
args = ["-y", "@coreui/docs-mcp", "--framework", "bootstrap", "--base-path", "/data-grid/docs"]
```
## Tools
Once connected, your assistant can call the following tools:
| Tool | Description |
| --- | --- |
| `list_components` | List documentation pages, optionally filtered by section or a substring. |
| `search_docs` | Search the documentation and return the best matching pages. |
| `get_doc_page` | Fetch the full Markdown of a page by slug or URL. |
| `get_component_api` | Get the structured API (options, events, methods) for the grid. |
## Configuration
| Flag | Environment variable | Default | Description |
| --- | --- | --- | --- |
| `--framework ` | `COREUI_DOCS_FRAMEWORKS` | `bootstrap,react,vue` | Enabled editions (comma-separated). The first is the default for tools. |
| `--base-url ` | `COREUI_DOCS_BASE_URL` | `https://coreui.io` | Origin of the CoreUI site. Override only for a staging or self-hosted mirror. |
| `--base-path ` | `COREUI_DOCS_BASE_PATH` | `/{framework}/docs` | Path after the origin where the docs live; `{framework}` is substituted. |
| `--docs-path ` | `COREUI_DOCS_PATHS` | — | Per-framework path overrides, `fw=path` comma-separated. |
| `--ttl ` | `COREUI_DOCS_TTL_MINUTES` | `360` | Cache freshness window. |
| — | `COREUI_DOCS_CACHE_DIR` | OS cache directory | On-disk cache location. |
## Example prompts
Once installed, try asking your AI assistant:
- "How do I enable row selection in CoreUI Data Grid?"
- "What options does CoreUI Data Grid accept?"
- "Show me the CoreUI Data Grid pagination documentation."
- "How do I export CoreUI Data Grid data to CSV?"
The package is open source and published as [`@coreui/docs-mcp`](https://www.npmjs.com/package/@coreui/docs-mcp).
---
# Data Grid Overview
> A single kitchen-sink CoreUI Data Grid demo — 10,000 rows across fourteen columns with the toolbar, per-column filters, sizing, pinning, selection and a column chooser all turned on at once.
This page is the kitchen-sink demo: one grid with every interaction feature
turned on at once, so you can see how they compose. It renders **10,000 rows**
across **fourteen columns** — four hidden by default — with the
[toolbar](https://coreui.io/data-grid/docs/features/toolbar/), [per-column filters](https://coreui.io/data-grid/docs/features/filtering/),
[column sizing](https://coreui.io/data-grid/docs/columns/sizing/), [pinning](https://coreui.io/data-grid/docs/columns/pinning/),
[ordering & visibility](https://coreui.io/data-grid/docs/columns/ordering-visibility/), the
[column menu](https://coreui.io/data-grid/docs/columns/menu/), [row selection](https://coreui.io/data-grid/docs/features/row-selection/),
multi-column [sorting](https://coreui.io/data-grid/docs/features/sorting/) and [pagination](https://coreui.io/data-grid/docs/features/pagination/).
Every one of these is a single option, documented on its own page — this demo
just enables them together.
```html
```
```js
const firstNames = ['Alice', 'Bob', 'Carol', 'Dave', 'Eve', 'Frank', 'Grace', 'Heidi', 'Ivan', 'Judy']
const lastNames = ['Smith', 'Jones', 'Brown', 'Taylor', 'Wilson', 'Davies', 'Evans', 'Thomas', 'Roberts', 'Walker']
const departments = ['Engineering', 'Sales', 'Marketing', 'Support', 'Finance', 'People']
const roles = ['Manager', 'Lead', 'Senior', 'Junior', 'Contractor']
const statuses = ['active', 'invited', 'suspended']
const countries = ['Poland', 'Germany', 'France', 'Spain', 'Italy', 'United States', 'United Kingdom']
const cities = ['Warsaw', 'Berlin', 'Paris', 'Madrid', 'Rome', 'New York', 'London']
const badges = { active: 'success', invited: 'info', suspended: 'danger' }
const items = Array.from({ length: 10000 }, (_, i) => ({
id: i + 1,
name: `${firstNames[i % firstNames.length]} ${lastNames[i % lastNames.length]}`,
email: `user${i + 1}@example.com`,
department: departments[i % departments.length],
role: roles[i % roles.length],
status: statuses[i % statuses.length],
salary: 45000 + ((i % 60) * 1500),
rating: ((i % 9) + 1) / 2,
projects: (i % 24) + 1,
country: countries[i % countries.length],
city: cities[i % cities.length],
startDate: new Date(2021, i % 12, (i % 28) + 1).toISOString(),
lastActive: new Date(2026, i % 6, (i % 27) + 1).toISOString(),
phone: `+1 555 ${String(1000 + (i % 9000))}`
}))
const currency = value => Number(value).toLocaleString('en-US', {
style: 'currency', currency: 'USD', maximumFractionDigits: 0
})
const date = value => new Date(value).toLocaleDateString('en-US')
new coreui.DataGrid(document.getElementById('dataGridOverview'), {
columns: [
{
key: 'id', label: '#', width: 72, hideable: false
},
{ key: 'name', label: 'Name', width: 180 },
{ key: 'email', label: 'Email', width: 220 },
{
key: 'department', label: 'Department', width: 150, filterType: 'select'
},
{
key: 'role', label: 'Role', width: 130, filterType: 'select'
},
{
key: 'status',
label: 'Status',
width: 130,
filterType: 'select',
render(item) {
return `${item.status} `
}
},
{
key: 'salary', label: 'Salary', width: 130, filterType: 'number', formatter: currency
},
{
key: 'rating', label: 'Rating', width: 110, filterType: 'number'
},
{
key: 'projects', label: 'Projects', width: 120, filterType: 'number'
},
{
key: 'country', label: 'Country', width: 160, filterType: 'select'
},
{ key: 'city', label: 'City', width: 150 },
{
key: 'startDate', label: 'Started', width: 140, filterType: 'date', formatter: date
},
{
key: 'lastActive', label: 'Last active', width: 140, filterType: 'date', formatter: date
},
{ key: 'phone', label: 'Phone', width: 160 }
],
items,
itemKey: item => String(item.id),
columnFilters: true,
columnMenu: true,
columnOrder: true,
columnPinning: { start: ['id'] },
columnSizing: true,
columnVisibility: {
projects: false, city: false, lastActive: false, phone: false
},
rowSelection: true,
sorting: { multiple: true },
pagination: { pageSize: 20, pageSizeOptions: [10, 20, 50, 100] },
toolbar: {
columns: true,
export: { filename: 'employees.csv' },
search: true
}
})
```
## What's turned on
| Option | Feature |
| --- | --- |
| `toolbar` | [Column chooser, CSV export and global search](https://coreui.io/data-grid/docs/features/toolbar/) |
| `columnFilters` + `filterType` | [Per-column typed filters](https://coreui.io/data-grid/docs/features/filtering/) (text, number, date, select) |
| `columnSizing` | [Drag-to-resize columns](https://coreui.io/data-grid/docs/columns/sizing/) |
| `columnPinning` | [`id` pinned to the start edge](https://coreui.io/data-grid/docs/columns/pinning/) |
| `columnOrder` | [Drag-and-drop column reordering](https://coreui.io/data-grid/docs/columns/ordering-visibility/) |
| `columnVisibility` | [Four columns hidden until you show them](https://coreui.io/data-grid/docs/columns/ordering-visibility/) |
| `columnMenu` | [Per-header sort / pin / hide menu](https://coreui.io/data-grid/docs/columns/menu/) |
| `rowSelection` | [Checkbox column with select-all](https://coreui.io/data-grid/docs/features/row-selection/) |
| `sorting: { multiple: true }` | [Shift-click multi-column sort](https://coreui.io/data-grid/docs/features/sorting/) |
| `pagination` | [Page size switcher](https://coreui.io/data-grid/docs/features/pagination/) |
## Presentation
`salary` and the two dates use a column [`formatter`](https://coreui.io/data-grid/docs/columns/overview/) so the
displayed value — and the [CSV export](https://coreui.io/data-grid/docs/features/csv-export/) — reads as currency
and localized dates. The `status` column uses [`render`](https://coreui.io/data-grid/docs/columns/overview/) to
draw a colored badge. Everything else is the raw value.
To scale past what fits in the browser, keep this UI and hand data fetching off to
your API with [server-side data](https://coreui.io/data-grid/docs/features/server-side-data/).
---
# Data Grid Virtualization
> Row virtualization renders only the visible window of rows, so the Data Grid stays fast with 100,000 rows and beyond — sorting, filtering and selection run across the full dataset.
Virtualization keeps the DOM small no matter how large the dataset is: only the
rows currently in view (plus a small buffer) are rendered as real elements.
Reach for it whenever you bind more rows than the browser can comfortably paint
at once — a few thousand and up. It is **on by default** (`virtualization: true`)
and is mutually exclusive with [pagination](https://coreui.io/data-grid/docs/features/pagination/).
## 100,000 rows, virtualized
Only the visible window of rows exists in the DOM — scroll, sort, filter and
select across the full dataset. This live demo runs on 100,000 generated rows.
```html
```
```js
const element = document.getElementById('dataGridVirtual')
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 statuses = ['active', 'pending', 'banned']
const items = Array.from({ length: 100000 }, (_, 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],
status: statuses[i % statuses.length],
score: (i * 37) % 1000
}
})
const start = performance.now()
new coreui.DataGrid(element, {
columns: [
{ key: 'id', label: '#', width: 90 },
{ key: 'name', label: 'Name', width: 200 },
{ key: 'email', label: 'Email', width: 260 },
{ key: 'role', label: 'Role', width: 110 },
{ key: 'status', label: 'Status', width: 110 },
{ key: 'score', label: 'Score', width: 90 }
],
items,
itemKey: item => String(item.id),
columnFilters: true,
globalFilter: true,
rowSelection: true
})
const meta = document.getElementById('dataGridVirtualMeta')
if (meta) {
meta.textContent = `100,000 rows initialized in ${Math.round(performance.now() - start)} ms`
}
```
## How it works
The grid measures the scroll viewport and renders only the rows that intersect
it. Two options tune the behavior:
- `rowHeight` — the estimated row height in px (default `44`) the virtualizer
uses to size the scroll area and decide how many rows fit.
- `overscan` — extra rows rendered above and below the visible window (default
`10`) to smooth fast scrolling. Raise it if you see blank rows while flinging;
lower it to shave DOM nodes.
Rows are a fixed `rowHeight` by default; turn on
[auto row height](#auto-row-height) below when they must grow with their
content. For datasets larger than browser memory, hand paging to your backend
with [server-side data](https://coreui.io/data-grid/docs/features/server-side-data/).
See the [Performance guide](https://coreui.io/data-grid/docs/guides/performance/) for tuning advice.
## Auto row height
Rows are a fixed `rowHeight` by default, and cells truncate with an ellipsis
rather than wrap — that is what lets the virtualizer place 100,000 rows
without measuring any of them.
Set `autoRowHeight` when rows must grow with their content instead. Cells wrap,
and the virtualizer measures each row as it renders:
```js
new DataGrid('#grid', {
columns,
items,
autoRowHeight: true,
rowHeight: 44
})
```
`rowHeight` keeps its job as the *estimate*: it sizes the scrollbar for rows
that have not been rendered yet, so a value close to the real average keeps
scrolling smooth. Measuring costs a layout read per rendered row, so leave it
off when every row is the same height.
Measurements are cached per row index and dropped whenever sorting, filtering,
paging or a data swap puts a different row at the same index.
Auto row height requires virtualization; it is ignored in pagination mode,
where the browser lays out every row anyway.
---
# Data Grid Sorting
> Sort the Data Grid by clicking a header, add multi-column sorting with shift+click, and opt individual columns out.
Sorting is **on by default**. Click a header to toggle ascending ↔ descending
(set `resetable: true` for a third click that clears the sort); the sort runs
across the whole dataset, not just the visible window. Shift+click a second
header to sort by more than one column at once.
```html
```
```js
const roles = ['admin', 'editor', 'viewer']
const items = Array.from({ length: 1000 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
role: roles[i % roles.length],
score: (i * 37) % 1000
}))
new coreui.DataGrid(document.getElementById('dataGridSorting'), {
columns: [
{
key: 'id', label: '#', width: 90, sortable: false
},
{ key: 'name', label: 'Name' },
{ key: 'role', label: 'Role', width: 140 },
{ key: 'score', label: 'Score', width: 120 }
],
items,
itemKey: item => String(item.id),
sorting: { multiple: true }, // shift+click a header to add a column to the sort
pagination: { pageSize: 10 }
})
```
## Usage
```js
new coreui.DataGrid(element, {
columns,
items,
sorting: true, // the default — pass an object to configure it
})
```
Pass an object to tune the behavior:
| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `multiple` | `boolean` | `true` | Allow sorting by more than one column with shift+click. |
| `resetable` | `boolean` | `false` | Allow a third click to clear the column's sort. |
Disable sorting for a single column with `sortable: false` in its
[definition](https://coreui.io/data-grid/docs/api/columns/), or turn it off entirely with `sorting: false`.
## Sort icon visibility
Only the **active** sort direction (the ascending/descending arrow) shows by
default — the neutral, unsorted indicator stays hidden to keep headers clean.
Set `sorterVisibility` to surface it on every sortable column:
| Value | Behavior |
| --- | --- |
| `'always'` | The neutral icon is always visible (dimmed) on sortable columns. |
| `'hover'` | The neutral icon appears when the header is hovered or focused. |
```js
new coreui.DataGrid(element, {
columns,
items,
sorterVisibility: 'hover',
})
```
## Multi-column sorting
With `multiple` enabled (the default), **shift+click** a second header to add it
to the sort instead of replacing the first. The sort priority follows click
order. Below, click **Department**, then shift+click **Salary** — the readout
shows the active sort and its priority.
```html
nClick a header, then shift+click another to sort by multiple columns.
```
```js
const departments = ['Engineering', 'Design', 'Sales']
const items = Array.from({ length: 1000 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
department: departments[i % departments.length],
salary: 40000 + ((i * 137) % 60000)
}))
const labels = { name: 'Name', department: 'Department', salary: 'Salary' }
const element = document.getElementById('dataGridSortingMulti')
const status = document.getElementById('dataGridSortingMultiStatus')
element.addEventListener('sortingChange.coreui.data-grid', event => {
status.textContent = event.sorting.length ?
`Sorted by: ${event.sorting
.map((sort, index) => `${index + 1}. ${labels[sort.id]} ${sort.desc ? '↓' : '↑'}`)
.join(' ')}` :
'Click a header, then shift+click another to sort by multiple columns.'
})
new coreui.DataGrid(element, {
columns: [
{
key: 'id', label: '#', width: 90, sortable: false
},
{ key: 'name', label: 'Name' },
{ key: 'department', label: 'Department', width: 160 },
{ key: 'salary', label: 'Salary', width: 140 }
],
items,
itemKey: item => String(item.id),
sorting: { multiple: true }, // shift+click a second header to add it to the sort
pagination: { pageSize: 10 }
})
```
## Reacting to sort changes
Each change emits `sortingChange.coreui.data-grid` with the grid's `{ sorting }`
state:
```js
element.addEventListener('sortingChange.coreui.data-grid', (event) => {
console.log(event.sorting) // [{ id: 'name', desc: false }]
})
```
In [server-side mode](https://coreui.io/data-grid/docs/features/server-side-data/) the same `sorting` state is
handed to your `dataProvider` so your API does the ordering.
---
# Data Grid Filtering
> Filter the Data Grid with per-column filter dialogs (typed operators, AND/OR, set filter), a global search input, custom filter UIs and custom matching predicates.
The Data Grid filters on two levels. `columnFilters: true` adds a filter button with a dialog
to every filterable column header; `globalFilter: true` adds a
single search input above the grid that matches across every column (the same
input as the [toolbar](https://coreui.io/data-grid/docs/features/toolbar/)'s `search` action). Both narrow
the rows client-side (or feed your [`dataProvider`](https://coreui.io/data-grid/docs/features/server-side-data/)
in server-side mode). Opt a column out with `filterable: false`.
```js
new coreui.DataGrid(element, {
columns,
items,
columnFilters: true, // per-column filter buttons + dialog
globalFilter: true, // single cross-column search input
})
```
## Filter menu
With `columnFilters` enabled every filterable column gets a **filter button**
(funnel icon, configurable via `filterIcon`) in its header, opening a filter
dialog. The dialog offers typed operators driven by `column.filterType` —
`text` (default), `number` and `date` get operator conditions (up to two,
joined **AND**/**OR**), while `select` renders a faceted checkbox list of the
column's actual values. An active filter marks the button with a dot; the
structured value travels inside the grid's `columnFilters` state, so a
[`dataProvider`](https://coreui.io/data-grid/docs/features/server-side-data/) receives it verbatim.
Try it below: filter *Salary* with `Between`, *Department* with the set
filter, or combine two conditions on *Hired*.
```html
```
```js
const departments = ['Engineering', 'Design', 'Sales', 'Support']
const items = Array.from({ length: 200 }, (_, i) => ({
name: `Employee ${String(i + 1).padStart(3, '0')}`,
department: departments[i % departments.length],
salary: 40_000 + ((i * 977) % 60_000),
hired: new Date(2020, i % 60, (i % 27) + 1).toISOString().slice(0, 10)
}))
new coreui.DataGrid(document.getElementById('dataGridFilterMenu'), {
columns: [
{ key: 'name', label: 'Name' },
{ key: 'department', label: 'Department', filterType: 'select' },
{ key: 'salary', label: 'Salary', filterType: 'number' },
{ key: 'hired', label: 'Hired', filterType: 'date' }
],
items,
columnFilters: true,
columnMenu: true,
pagination: { pageSize: 8 },
sorting: false,
virtualization: false
})
```
## Custom column filters
Two per-column hooks customize filtering. `filter` renders your own UI in a
dedicated filter row (shown only for columns that define it) — the same factory contract as slots, and the
`column` hands you `setFilterValue()`, `getFilterValue()` and
`column.getFacetedUniqueValues()` for building selects from the actual data.
`filterFn` swaps the matching logic (default: case-insensitive contains, or the
[filter dialog](https://coreui.io/data-grid/docs/features/filtering/#filter-menu)'s structured value) for your
own predicate `(value, filterValue, item) => boolean` — with a custom UI it
receives exactly what that UI committed. Here Role pairs a faceted select with
an exact-match predicate, while Score just declares `filterType: 'number'` and
gets the built-in numeric dialog.
```html
```
```js
const roles = ['admin', 'editor', 'viewer']
const items = Array.from({ length: 1000 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
role: roles[i % roles.length],
score: (i * 37) % 1000
}))
new coreui.DataGrid(document.getElementById('dataGridCustomFilters'), {
columnFilters: true,
columns: [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email', style: { width: '30%' } },
{
key: 'role',
label: 'Role',
width: 160,
// Exact match - the default contains would also match partial values.
filterFn: (value, filterValue) => value === filterValue,
filter({ column }) {
const select = document.createElement('select')
select.className = 'form-select form-select-sm'
for (const value of ['', ...[...column.getFacetedUniqueValues().keys()].toSorted()]) {
const option = document.createElement('option')
option.value = value
option.textContent = value === '' ? 'All' : value
select.append(option)
}
select.addEventListener('change', () => column.setFilterValue(select.value || undefined))
return { element: select }
}
},
{
key: 'score', label: 'Score', width: 140, filterType: 'number'
}
],
items,
itemKey: item => String(item.id),
pagination: { pageSize: 10 }
})
```
## Custom filter API
| Column key | Type | Description |
| --- | --- | --- |
| `filterable` | `boolean` | Set `false` to remove the column's filter button. |
| `filter` | `({ column, table, labels }) => { element, update?, dispose? }` | Custom filter UI rendered in the filter row instead of the filter button (requires `columnFilters`). |
| `filterFn` | `(value, filterValue, item) => boolean` | Custom predicate replacing the built-in matching; with the filter dialog it receives the structured value. |
Every filter change emits `filterChange.coreui.data-grid` with
`{ columnFilters, globalFilter }`. A custom [toolbar slot](https://coreui.io/data-grid/docs/features/slots/) that
hosts its own search box still needs `globalFilter: true` for the query to reach
the grid.
---
# Data Grid Row Reordering
> Let users drag rows into a new order with a per-row grip — the Data Grid hands you the reordered array and never mutates your data.
Some lists have an order that only a person can decide: a task backlog, a
playlist, the steps of a workflow. `rowOrder` gives each row a grip, and the
grid tells you where the row was dropped — you own the data, so you own the
new order.
## Row reordering
`rowOrder: true` adds a narrow handle column before the data columns. Only the
grip starts a drag: a row is full of interactive content — checkboxes, editors,
custom cells — that a whole-row handle would swallow.
```html
```
```js
const items = [
{
id: 1, task: 'Draft the release notes', owner: 'Alice', status: 'In progress'
},
{
id: 2, task: 'Review the API surface', owner: 'Bob', status: 'Blocked'
},
{
id: 3, task: 'Ship the beta', owner: 'Carol', status: 'Todo'
},
{
id: 4, task: 'Update the changelog', owner: 'Dave', status: 'Todo'
}
]
const grid = new coreui.DataGrid(document.getElementById('dataGridRowOrder'), {
columns: [
{ key: 'task', label: 'Task' },
{ key: 'owner', label: 'Owner' },
{ key: 'status', label: 'Status' }
],
items,
itemKey: item => String(item.id),
rowOrder: true,
sorting: false,
virtualization: false
})
document
.getElementById('dataGridRowOrder')
.addEventListener('rowOrderChange.coreui.data-grid', event => {
grid.setItems(event.items)
})
```
## The contract
The grid **never mutates `items`**. Dropping a row emits
`rowOrderChange.coreui.data-grid` with everything needed to apply the move —
the same contract [inline editing](https://coreui.io/data-grid/docs/features/editing/) uses:
| Payload | Description |
| --- | --- |
| `items` | The full array in its new order, ready to hand back through `setItems()` or send to your API. |
| `item` | The row that moved. |
| `oldIndex` | Its index in `items` before the move. |
| `newIndex` | Its index after it. |
Until you apply it, nothing changes on screen. That is deliberate: the order is
usually persisted, and a row that snaps back when the request fails is better
than one that lies.
The indexes are positions in `items`, not in the rendered window — in
[pagination](https://coreui.io/data-grid/docs/features/pagination/) mode the page offset is already added, so
dragging the first row of page 2 reports `oldIndex: 10`, not `0`.
## The drag interaction
Row dragging is the same pointer-based controller as
[column dragging](https://coreui.io/data-grid/docs/columns/ordering-visibility/#the-drag-interaction), turned
on its side: a ghost with the row's first cell, a live preview where the rows
it passes slide aside and it slides into the gap, edge auto-scroll for long
lists, and Escape to cancel. It works the same with a mouse, a pen and a
finger, and honours `prefers-reduced-motion`.
## Options
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `rowOrder` | `boolean` | `false` | Adds a drag handle per row for reordering. |
| `rowHandleIcon` | `string` | grip | Custom handle icon (SVG string), sanitized like every other icon. |
The handle's accessible name comes from `labels.reorderRow` — see
[localization](https://coreui.io/data-grid/docs/customization/localization/).
---
# Data Grid Row Selection
> Add a checkbox column to the Data Grid with select-all and shift+click range selection, keyed to a stable row id so selection survives sorting, filtering and paging.
`rowSelection` adds a checkbox column with a select-all header and shift+click
range selection. Selection is keyed by [`itemKey`](https://coreui.io/data-grid/docs/api/options/), so a selected
row stays selected as the user sorts, filters or pages — set `itemKey` whenever
you enable selection. Select a few rows below, then page or sort — the selection
holds.
```html
No rows selected
```
```js
const roles = ['admin', 'editor', 'viewer']
const items = Array.from({ length: 1000 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
role: roles[i % roles.length]
}))
const element = document.getElementById('dataGridRowSelection')
new coreui.DataGrid(element, {
columns: [
{ key: 'id', label: '#', width: 90 },
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email', style: { width: '30%' } },
{ key: 'role', label: 'Role', width: 110 }
],
items,
itemKey: item => String(item.id), // required for stable selection
rowSelection: true,
pagination: { pageSize: 10 }
})
element.addEventListener('selectionChange.coreui.data-grid', event => {
const count = event.selectedItems.length
document.getElementById('dataGridRowSelectionMeta').textContent =
count ? `${count} row${count === 1 ? '' : 's'} selected` : 'No rows selected'
})
```
## Usage
```js
new coreui.DataGrid(element, {
columns,
items,
itemKey: (item) => String(item.id), // required for stable selection
rowSelection: true,
})
```
Pass an object to configure it:
| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `selectAll` | `boolean` | `true` | Show the select-all checkbox in the header. |
The [pagination demo](https://coreui.io/data-grid/docs/features/pagination/) shows selection in action alongside
a custom actions column.
## Reading the selection
Call `grid.getSelectedItems()` for the selected objects, or listen for changes:
```js
element.addEventListener('selectionChange.coreui.data-grid', (event) => {
console.log(event.selectedItems)
console.log(event.rowSelection) // row-selection state
})
```
## With pinning and server-side data
When a column is pinned to the start and selection is on, the checkbox column travels
with it — see [Column pinning](https://coreui.io/data-grid/docs/columns/pinning/). In
[server-side mode](https://coreui.io/data-grid/docs/features/server-side-data/), selection is id-keyed so it
survives page changes, but `getSelectedItems()` returns only the items present
in the current page's data.
---
# Data Grid Cell Selection
> Spreadsheet-style cell ranges in the Data Grid — shift-click, drag and shift+arrows, with Ctrl/Cmd+C copying the range as tab-separated text ready to paste into Excel.
People who work with tabular data reach for a spreadsheet's muscle memory:
drag across a block of numbers, hit Ctrl+C, paste it somewhere else.
`cellSelection` gives the grid that, on top of the same active-cell substrate
[keyboard navigation](https://coreui.io/data-grid/docs/features/keyboard-navigation/) already provides.
## Cell selection
Select the way you would in a spreadsheet — click a cell, drag across a block,
shift-click a far corner, or hold Shift and press an arrow. Ctrl/Cmd+A selects
everything, and **Ctrl/Cmd+C copies the range**: try it and paste into a
spreadsheet.
```html
```
```js
new coreui.DataGrid(document.getElementById('dataGridCellSelection'), {
columns: [
{ key: 'quarter', label: 'Quarter' },
{ key: 'emea', label: 'EMEA' },
{ key: 'amer', label: 'AMER' },
{ key: 'apac', label: 'APAC' }
],
items: [
{
quarter: 'Q1', emea: 128400, amer: 214900, apac: 96300
},
{
quarter: 'Q2', emea: 141200, amer: 208700, apac: 104800
},
{
quarter: 'Q3', emea: 152900, amer: 226400, apac: 118200
},
{
quarter: 'Q4', emea: 167300, amer: 241800, apac: 129600
}
],
cellSelection: true,
sorting: false,
virtualization: false
})
```
## Interactions
| Input | Result |
| --- | --- |
| Click / drag | Starts a range and extends it while dragging. |
| Shift + click | Extends the range to the clicked cell, keeping the anchor. |
| Shift + arrow | Extends the range one cell, keeping the anchor. |
| Arrow | Moves the active cell and collapses the range to it. |
| Ctrl/Cmd + click | Adds or subtracts a second rectangle. |
| Ctrl/Cmd + A | Selects every cell. |
| Ctrl/Cmd + C | Copies the selection to the clipboard. |
`cellSelection` implies `cellNavigation`: the active cell is the grid's single
focus owner, and the selection anchors on it rather than tracking a second one.
## The clipboard format
The copy is **tab-separated** — the format spreadsheets read back as columns.
Rows are separated by newlines, and disjoint rectangles by a blank line. A value
containing a tab, a newline or a quote is quoted the way TSV consumers expect.
Cells copy as they are **displayed**: a column's `formatter` runs first, so a
formatted date pastes as the date on screen rather than its underlying value.
Each copy fires `cellCopy.coreui.data-grid` with the exact text written, so you
can mirror it somewhere else or count it.
The grid uses the async clipboard API and falls back to a hidden textarea where
that is unavailable or denied — both work because the copy runs inside the key
press that asked for it.
## Options
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `cellSelection` | `boolean` | `false` | Enables cell ranges, clipboard copy and select-all. Implies `cellNavigation`. |
## Bundle cost
Cell selection is the largest single feature in the grid: `cellSelectionFeature`
adds about **4 KB gzipped** to the TanStack core. A grid that does not need it
can drop that by naming a smaller
[feature set](https://coreui.io/data-grid/docs/customization/feature-sets/) — which is exactly what the lite
build does.
---
# Data Grid Keyboard Navigation
> APG grid keyboard navigation for CoreUI Data Grid — role="grid", a roving-tabindex active cell and arrow-key movement across header and data cells.
`cellNavigation` switches the grid to the ARIA
[grid pattern](https://www.w3.org/WAI/ARIA/apg/patterns/grid/): the table gains
`role="grid"`, exactly one cell is tabbable at a time (a roving tabindex), and
the arrow keys move a visible active cell across header and data cells. Click a
cell below, then navigate with the keyboard.
```html
```
```js
const roles = ['admin', 'editor', 'viewer']
const items = Array.from({ length: 200 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
role: roles[i % roles.length]
}))
new coreui.DataGrid(document.getElementById('dataGridKeyboardNavigation'), {
columns: [
{ key: 'id', label: '#', width: 90 },
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email', style: { width: '30%' } },
{ key: 'role', label: 'Role', width: 110 }
],
items,
itemKey: item => String(item.id),
cellNavigation: true,
rowSelection: true
})
```
## Usage
```js
new coreui.DataGrid(element, {
columns,
items,
cellNavigation: true,
})
```
`cellNavigation` is off by default — without it the grid keeps native table
semantics and the [accessible chrome](https://coreui.io/data-grid/docs/guides/accessibility/) it always had.
[Inline editing](https://coreui.io/data-grid/docs/features/editing/) requires the active-cell model, so
`editing: true` enables `cellNavigation` automatically.
## Keys
| Key | Action |
| --- | --- |
| Arrow keys | Move one cell; stop at the edges (no wrap). The header label row is row one — Arrow Up from the first data row reaches it. |
| Home / End | First / last cell in the row. |
| Ctrl+Home / Ctrl+End | First / last data cell of the grid. |
| PageUp / PageDown | Move one viewport when virtualized; move one page (and flip it) under pagination. |
| Tab / Shift+Tab | Leave the grid — the whole grid is a single tab stop. |
| Enter | Toggle sort on a sortable header cell; descend into a data cell's interactive content (links, buttons); start [editing](https://coreui.io/data-grid/docs/features/editing/) an editable cell. |
| Escape | Ascend from cell content back to the cell. |
| Space | Toggle [row selection](https://coreui.io/data-grid/docs/features/row-selection/) on the active row. |
## Focus and virtualization
The active cell is state, not DOM: with
[virtualization](https://coreui.io/data-grid/docs/features/virtualization/) on, navigating to an off-window row
scrolls it into view first and focuses it once it renders. If the focused row is
recycled out of the window while you scroll, focus parks on the viewport and the
next navigation key brings the active cell back into view.
## Styling
The active cell shows an inset focus ring driven by two component tokens:
```css
.data-grid {
--cui-data-grid-focus-ring-width: 2px;
--cui-data-grid-focus-ring-color: var(--cui-primary);
}
```
---
# Data Grid Inline Editing
> Inline cell editing for CoreUI Data Grid — built-in text, number and select editors, validation, and a popup contract for rich editors like date pickers and multi-selects.
`editing` turns cells editable in place. Press Enter or F2
on the active cell — or double-click any cell — to start; Enter
commits, Escape cancels, Tab commits and moves to the next
editable cell. Editing builds on
[keyboard navigation](https://coreui.io/data-grid/docs/features/keyboard-navigation/), so `editing: true`
enables `cellNavigation` automatically.
```html
```
```js
const roles = ['admin', 'editor', 'viewer']
let items = Array.from({ length: 200 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
age: 20 + (i % 40),
role: roles[i % roles.length]
}))
const element = document.getElementById('dataGridEditing')
const grid = new coreui.DataGrid(element, {
columns: [
{ key: 'id', label: '#', width: 90 },
{
key: 'name', label: 'Name', editable: true, editValidate: value => (value === '' ? 'Name is required' : true)
},
{
key: 'age', label: 'Age', width: 110, editable: { type: 'number', min: 0, max: 120 }
},
{
key: 'role', label: 'Role', width: 130, editable: { type: 'select', options: roles }
}
],
items,
itemKey: item => String(item.id),
editing: true
})
// The grid never mutates items - apply the committed change yourself.
// Replace the row and the array: the row model and cell values are memoized
// by identity, so an in-place mutation would keep showing the old value.
element.addEventListener('editCommit.coreui.data-grid', event => {
items = items.map(row => (row === event.item ? { ...row, [event.columnId]: event.value } : row))
grid.setItems(items)
})
```
## Usage
Editing is opt-in per column — `editable` picks a built-in editor, a custom
`editor` factory is itself the opt-in:
```js
new coreui.DataGrid(element, {
editing: true,
columns: [
{ key: 'name', editable: true }, // text input
{ key: 'age', editable: { type: 'number', min: 0 } }, // number input
{ key: 'role', editable: { type: 'select', options: ['admin', 'user'] } },
],
})
```
## The app owns the data
The grid never mutates `items`. A commit fires `editCommit` with
`{ item, columnId, value, previousValue }` — apply the change and hand the data
back (client-side via [`setItems()`](https://coreui.io/data-grid/docs/api/methods/), server-side by PATCHing and
refetching):
```js
element.addEventListener('editCommit.coreui.data-grid', (event) => {
// Replace the row and the array - the row model and cell values are
// memoized by identity, so an in-place mutation keeps showing the old value.
items = items.map((row) => (row === event.item ? { ...row, [event.columnId]: event.value } : row))
grid.setItems(items) // state-preserving swap
})
```
`editStart` and `editCancel` fire around it with `{ item, columnId }`.
## Validation
`editValidate` gates the commit. Return `true` to accept, or a message to block
it — the editor gets `aria-invalid`, the `is-invalid` class and an
`aria-errormessage` pointing at the message:
```js
{ key: 'name', editable: true, editValidate: (value, item) => value !== '' || 'Name is required' }
```
An invalid value keeps the editor open; Escape still cancels.
## Custom editors
`editor` replaces the built-in input with your own UI. It receives the editing
context and returns the editor contract:
```js
{
key: 'note',
editor: ({ item, column, value, commit, cancel, labels }) => {
const input = document.createElement('input')
input.className = 'form-control form-control-sm'
input.value = String(value ?? '')
return {
element: input,
focus: () => input.select(),
getValue: () => input.value,
}
}
}
```
| Key | Description |
| --- | --- |
| `element` | The editor's root element, rendered inside the cell (or the popup layer). |
| `getValue?` | Returns the draft — feeds the Enter/Tab/blur/outside commits. Without it the grid can only cancel from the outside; call `commit(value)` yourself. |
| `focus?` | Called once the editor is in the DOM; defaults to focusing the first form control. |
| `contains?` | Extends the edit scope to elements outside `element` — overlays portaled to `body`. |
| `dispose?` | Cleanup on commit/cancel. |
| `popup?` | Render in an overlay anchored to the cell instead of inline (see below). |
## Rich editors — date pickers, autocompletes, multi-selects
Editors whose UI extends beyond the cell — a date range picker's calendar, a
multi-select's listbox — return `popup: true`. The grid renders them in a
`.data-grid-editor-popup` layer anchored to the cell (min-width = cell width),
so a tall editor never reflows the row, and the cell keeps its content
underneath.
Three rules make components like `@coreui/coreui-pro`'s date range picker,
time picker, autocomplete and multi-select work as editors:
- **Commit on outside, not on blur alone.** Picking a date in a calendar blurs
the input mid-edit — so the grid commits on pointerdown *outside the edit
scope* (cell + editor + popup + anything `contains()` claims). Pin the
component's `container` to the editor element, or claim a portaled overlay
via `contains()`.
- **One Escape, one layer.** An Escape the editor consumed (calling
`preventDefault()` to close its own overlay) never cancels the edit — only
the next, unconsumed Escape does.
- **Values are not scalars.** `getValue()` can return anything — a range
picker commits `{ startDate, endDate }`, a multi-select commits an array;
`editCommit` passes it through untouched.
```js
{
key: 'period',
editor: ({ value, commit }) => {
const element = document.createElement('div')
const picker = new coreui.DateRangePicker(element, {
startDate: value?.startDate,
endDate: value?.endDate,
container: element, // keep the calendar inside the edit scope
})
return {
element,
popup: true,
getValue: () => ({ startDate: picker.startDate, endDate: picker.endDate }),
dispose: () => picker.dispose(),
}
}
}
```
## Interaction details
- Scrolling the editing row out of the
[virtualized](https://coreui.io/data-grid/docs/features/virtualization/) window commits the draft; so do
sort, filter and page transitions.
- `setItems()` and [server-side](https://coreui.io/data-grid/docs/features/server-side-data/) data loads cancel
an in-flight edit — the incoming data owns the cell.
- Shift +Tab commits and moves to the previous editable
cell.
---
# Data Grid Undo & Redo
> Undo and redo inline-edit commits in the CoreUI Data Grid — toolbar buttons and Ctrl+Z / Ctrl+Shift+Z / Ctrl+Y, with the app staying the single source of truth.
`history` keeps an undo/redo stack of [inline editing](https://coreui.io/data-grid/docs/features/editing/)
commits. Undo with the toolbar button or Ctrl +Z
(⌘Z on macOS), redo with Ctrl +Shift +Z
or Ctrl +Y . Edit a few cells below, then undo your way
back.
```html
```
```js
const roles = ['admin', 'editor', 'viewer']
let items = Array.from({ length: 200 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
age: 20 + (i % 40),
role: roles[i % roles.length]
}))
const element = document.getElementById('dataGridHistory')
const grid = new coreui.DataGrid(element, {
columns: [
{ key: 'id', label: '#', width: 90 },
{ key: 'name', label: 'Name', editable: true },
{
key: 'age', label: 'Age', width: 110, editable: { type: 'number', min: 0, max: 120 }
},
{
key: 'role', label: 'Role', width: 130, editable: { type: 'select', options: roles }
}
],
items,
itemKey: item => String(item.id),
editing: true,
history: true,
toolbar: { history: true }
})
// The grid never mutates items - undo/redo re-emit editCommit with the
// values swapped, so this one handler covers edits AND their reversal.
element.addEventListener('editCommit.coreui.data-grid', event => {
items = items.map(row => (row === event.item ? { ...row, [event.columnId]: event.value } : row))
grid.setItems(items)
})
```
## Usage
```js
new coreui.DataGrid(element, {
columns,
items,
editing: true,
history: true, // track edit commits
toolbar: { history: true }, // undo/redo buttons
})
```
The buttons stay disabled while their stack is empty; each undo/redo announces
through the ARIA live region (`undoneAnnouncement`/`redoneAnnouncement`
labels). `grid.undo()` / `grid.redo()` are also callable directly.
## How undo works — the app stays in charge
The grid never mutates `items`. An undo **re-emits `editCommit`** with `value`
and `previousValue` swapped (redo re-emits the original), so the same handler
that applied the edit reverts it — no second code path:
```js
element.addEventListener('editCommit.coreui.data-grid', (event) => {
items = items.map((row) => (row === event.item ? { ...row, [event.columnId]: event.value } : row))
grid.setItems(items)
})
```
History entries reference rows by id, so they survive the immutable updates
this pattern produces. If a row disappears from the data entirely, its entries
are dropped. New commits clear the redo stack; the stack holds the last 100
edits. Following MUI and AG Grid, undo/redo covers **data edits** — view
changes (sorting, filters, column layout) are not tracked; persist those with
[`stateKey`](https://coreui.io/data-grid/docs/features/state/) instead.
---
# Data Grid Save & Restore State
> Persist the CoreUI Data Grid view — sorting, filters, column order, sizing, visibility, pinning, selection and page — to localStorage with a single option, or snapshot it programmatically.
`stateKey` makes the grid's view persistent: every user-adjustable state slice
— sorting, column filters and the global search, column order, sizing,
visibility and pinning, row selection and the page — **autosaves to
`localStorage`** under the key (debounced) and **restores automatically on the
next visit**. No buttons, nothing to wire up. Sort or resize below, reload the
page — the grid comes back as you left it.
```html
```
```js
const roles = ['admin', 'editor', 'viewer']
const items = Array.from({ length: 200 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
role: roles[i % roles.length]
}))
new coreui.DataGrid(document.getElementById('dataGridState'), {
columns: [
{ key: 'id', label: '#', width: 90 },
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email', style: { width: '30%' } },
{ key: 'role', label: 'Role', width: 110 }
],
items,
itemKey: item => String(item.id),
columnVisibility: true,
columnSizing: true,
globalFilter: true,
stateKey: 'docs-state-demo',
toolbar: true
})
```
## Usage
```js
new coreui.DataGrid(element, {
columns,
items,
itemKey: (item) => String(item.id), // keeps restored selection meaningful
stateKey: 'users-grid', // localStorage identity — that's it
})
```
Every state change writes the snapshot (debounced at 250 ms) to
`localStorage` under `coreui-data-grid:`; the next grid constructed
with the same key restores it before first render.
## Programmatic snapshots
The persistence layer is optional — the snapshot API works without `stateKey`:
```js
const state = grid.getState() // serializable: JSON.stringify(state) is safe
grid.restoreState(state) // applies any subset of the slices
```
`getState()` returns
`{ sorting, columnFilters, globalFilter, columnOrder, columnPinning, columnSizing, columnVisibility, pagination, rowSelection }`.
Hand it to your own storage (a user-profile API, the URL) and feed it back
through `restoreState()` — partial snapshots are fine, absent slices keep
their current values.
## What is (and isn't) state
- Row selection restores by row id — set [`itemKey`](https://coreui.io/data-grid/docs/api/options/) or the
restored ids point at positions, not rows.
- In [server-side mode](https://coreui.io/data-grid/docs/features/server-side-data/) the restored
sorting/filters/page are applied **before the first request**, so the grid
loads straight into the saved view.
- Scroll position and an in-flight [edit](https://coreui.io/data-grid/docs/features/editing/) draft are not
state. Data edits aren't either — undoing those is what
[undo & redo](https://coreui.io/data-grid/docs/features/history/) is for.
- Restoring fires the matching `*Change` events for every slice that changed —
your app sees a restore exactly like user interaction.
---
# Data Grid Pagination
> Page through the Data Grid with a page-size selector, range info and CoreUI pagination controls instead of virtualized scrolling.
Pagination breaks the dataset into fixed-size pages with familiar
Previous/Next controls. Use it when users expect discrete pages, when you want a
predictable page height on the screen, or as the client-side counterpart to
[server-side data](https://coreui.io/data-grid/docs/features/server-side-data/) (which always paginates).
Pagination is **mutually exclusive with [virtualization](https://coreui.io/data-grid/docs/features/virtualization/)** —
turning it on switches the grid out of windowed scrolling.
## Pagination mode
Instead of virtualization the grid can paginate — with a page-size selector,
range info and CoreUI `.pagination` controls. This demo also shows a custom
`formatter`, an actions column built with `render`, and row selection.
```html
```
```js
const roles = ['admin', 'editor', 'viewer']
const items = Array.from({ length: 1000 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
role: roles[i % roles.length]
}))
new coreui.DataGrid(document.getElementById('dataGridPagination'), {
columns: [
{ key: 'id', label: '#', width: 90 },
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email', style: { width: '30%' } },
{
key: 'role', label: 'Role', width: 110, formatter: value => String(value).toUpperCase()
},
{
key: 'actions',
label: '',
sortable: false,
filterable: false,
width: 120,
render(item) {
const button = document.createElement('button')
button.type = 'button'
button.className = 'btn btn-sm btn-outline-primary'
button.textContent = 'Edit'
button.addEventListener('click', () => alert(`Edit ${item.name} (#${item.id})`))
return button
}
}
],
items,
itemKey: item => String(item.id),
pagination: { pageSize: 10 },
rowSelection: true
})
```
## Options
Pass `pagination: true` for the defaults, or an object to configure it:
| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `pageSize` | `number` | `10` | Rows per page. |
| `pageSizeOptions` | `number[]` | `[5, 10, 20, 50]` | Choices in the page-size selector. |
| `position` | `'top' \| 'bottom' \| 'both'` | `'bottom'` | Where the pagination bar renders. |
| `info` | `boolean` | `true` | Shows the `Showing X–Y of Z` range summary. |
Every page change emits `paginationChange.coreui.data-grid` with the grid's
`{ pagination }` state. Drive paging yourself through the
[headless table](https://coreui.io/data-grid/docs/api/headless/) — e.g. `grid.table.setPageIndex(3)`.
---
# Data Grid Server-Side Data
> Delegate sorting, filtering and pagination to your API with a single dataProvider function — the Data Grid fetches one debounced request per state change and drops stale responses.
Client-side mode ends where the browser's memory does. When your dataset lives
in a database with tens of thousands of rows or more, hand the work to your
backend: set `dataProvider` and the grid stops computing row models locally and
asks your API for each page instead. This is the single biggest adoption
unlocker for large data.
## Server-side data
Set `dataProvider` and the grid switches to server-side mode — every sorting,
filtering or pagination change triggers one debounced request (stale responses
are dropped automatically), a loading overlay covers the viewport and totals
come from your `totalRows`. Server-side mode implies pagination. This live demo
hits CoreUI's public demo API (`apitest.coreui.io/demos/users`) — 10,000+
records sorted, filtered and paged server-side.
```html
```
```js
new coreui.DataGrid(document.getElementById('dataGridServer'), {
columns: [
{ key: 'first_name', label: 'First name' },
{ key: 'last_name', label: 'Last name' },
{ key: 'email', label: 'Email' },
{ key: 'country', label: 'Country' },
{ key: 'ip_address', label: 'IP' }
],
itemKey: item => String(item.id),
columnFilters: true,
async dataProvider({ sorting, columnFilters, pagination }) {
const params = new URLSearchParams({
offset: String(pagination.pageIndex * pagination.pageSize),
limit: String(pagination.pageSize)
})
for (const { id, value } of columnFilters) {
params.append(id, String(value))
}
const [sort] = sorting
if (sort) {
params.append('sort', `${sort.id}%${sort.desc ? 'desc' : 'asc'}`)
}
const response = await fetch(`https://apitest.coreui.io/demos/users?${params}`)
const result = await response.json()
const totalRows = Number(result.number_of_matching_records)
return { items: totalRows ? result.records : [], totalRows }
}
})
```
## The dataProvider contract
```js
new coreui.DataGrid(element, {
columns,
dataProvider: async ({ sorting, columnFilters, globalFilter, pagination }) => {
// fetch from your API and return the matching page
return { items, totalRows }
},
pagination: true, // server-side mode implies pagination
})
```
- **One request per state change.** `sorting`, `columnFilters`, `globalFilter`
and `pagination` arrive as structured state; return the page of `items` plus the
full `totalRows` count so the pager can render.
- **Debounced & race-safe.** Rapid changes coalesce into one request and only
the latest response is applied — stale ones are dropped by request id.
- **Loading UX.** A `.data-grid-loading` overlay with a spinner covers the
viewport and `aria-busy` is set while a request is in flight
(`labels.loading`).
- **Errors.** A rejected fetch emits `dataError.coreui.data-grid` `{ error }`,
shows the empty state (`labels.loadError`) and leaves the grid interactive.
- **Success.** Each load emits `dataLoad.coreui.data-grid` `{ items, totalRows }`.
## Selection semantics
`rowSelection` is keyed by [`itemKey`](https://coreui.io/data-grid/docs/api/options/), so a selection survives
page changes by design. `getSelectedItems()` returns only the items present in
the current page's data — the grid does not cache full objects for pages it has
scrolled past.
The same page-bound rule applies to [CSV export](https://coreui.io/data-grid/docs/features/csv-export/): in
server-side mode every scope (`filtered`, `all`, `selected`) exports the
currently loaded page only.
When the [filter menu](https://coreui.io/data-grid/docs/features/filtering/) is used, a column's entry in
`columnFilters` carries the structured value — `{ conditions: [{ operator,
value, value2? }], join: 'and' | 'or' }` for typed operators or `{ operator:
'in', value: [...] }` for the set filter — instead of a plain string. Servers
should handle both shapes.
---
# Data Grid Infinite Scroll
> Load a server-side dataset page by page as the user scrolls — the Data Grid appends each page to the rows already in view and keeps virtualization on instead of switching to a pager.
[Server-side data](https://coreui.io/data-grid/docs/features/server-side-data/) normally replaces the rows on
every page change: you get a pager, and page 2 makes page 1 disappear. Infinite
scroll keeps them. Each request appends to a growing buffer behind the
[virtualizer](https://coreui.io/data-grid/docs/features/virtualization/), so the user scrolls one continuous
list while the grid quietly fetches what is below the fold.
## Infinite scroll
Set `infiniteScroll` alongside `dataProvider`. There is no pager: the grid
requests the next page when the last rendered row comes within `threshold` rows
of the end of what is loaded, and stops once `totalRows` is reached. This live
demo hits CoreUI's public demo API (`apitest.coreui.io/demos/users`) — scroll to
pull in the next 50 records.
```html
```
```js
new coreui.DataGrid(document.getElementById('dataGridInfinite'), {
columns: [
{ key: 'first_name', label: 'First name' },
{ key: 'last_name', label: 'Last name' },
{ key: 'email', label: 'Email' },
{ key: 'country', label: 'Country' },
{ key: 'ip_address', label: 'IP' }
],
itemKey: item => String(item.id),
infiniteScroll: { pageSize: 50 },
async dataProvider({ sorting, pagination }) {
const params = new URLSearchParams({
offset: String(pagination.pageIndex * pagination.pageSize),
limit: String(pagination.pageSize)
})
const [sort] = sorting
if (sort) {
params.append('sort', `${sort.id}%${sort.desc ? 'desc' : 'asc'}`)
}
const response = await fetch(`https://apitest.coreui.io/demos/users?${params}`)
const result = await response.json()
const totalRows = Number(result.number_of_matching_records)
return { items: totalRows ? result.records : [], totalRows }
}
})
```
## Options
```js
new coreui.DataGrid(element, {
columns,
dataProvider,
infiniteScroll: {
pageSize: 50, // rows per request (default 50)
threshold: 10 // rows left below the last rendered one (defaults to overscan)
}
})
```
`infiniteScroll: true` takes both defaults. `pageSize` is what the grid sends
as `pagination.pageSize`, so your `dataProvider` contract does not change — the
same offset/limit handler serves both modes.
## Requirements
Infinite scroll is a mode, not a modifier, and the grid throws on a
contradictory setup rather than silently picking one:
- **`dataProvider` is required.** A local `items` array is already fully
rendered by virtualization; there is nothing to fetch.
- **`pagination` cannot be combined with it.** Both own the page index, so the
grid rejects the pair.
- **`virtualization` is required** (it is on by default). The trigger is a row
index in the rendered window, which is what makes it work with
[auto row height](https://coreui.io/data-grid/docs/features/virtualization/#auto-row-height).
## Behavior
- **The buffer only grows.** `pageIndex` 0 replaces it; every later page
appends. Rows already on screen keep their index, their measured height, their
selection and the keyboard focus.
- **Sorting, filters and search reset it.** Any of them returns to page 0,
replaces the buffer and scrolls back to the top — the old rows no longer
describe the new query.
- **Loading UX.** The first page shows the usual `.data-grid-loading` overlay.
Every later page shows `.data-grid-loading-more`, a sticky strip at the bottom
of the viewport, so the rows already loaded stay readable.
- **Events.** Each page emits `dataLoad.coreui.data-grid` `{ items, totalRows }`
with that page's items. `paginationChange.coreui.data-grid` does **not** fire:
pagination is internal here, with no widget and nothing for the user to set.
- **`aria-rowcount`** is the full `totalRows`, and each row's `aria-rowindex` is
its absolute position — assistive technology announces "row 60 of 10,000"
while only 60 rows exist in the DOM.
## Selection and export
Both stay page-bound, exactly as in [server-side
mode](https://coreui.io/data-grid/docs/features/server-side-data/#selection-semantics): `getSelectedItems()` and
every [CSV](https://coreui.io/data-grid/docs/features/csv-export/) or [Excel](https://coreui.io/data-grid/docs/features/excel-export/) scope see
the rows currently in the buffer. With infinite scroll that buffer is everything
scrolled so far rather than a single page, so it grows as the user goes.
---
# Data Grid Toolbar
> Add a built-in Data Grid toolbar with a column chooser, CSV export button and global search — enabled with a single option or configured granularly.
The `toolbar` option adds a built-in chrome row above the grid with a
**column chooser**, a **CSV export** button, **Undo/Redo** buttons (with
[`history`](https://coreui.io/data-grid/docs/features/history/)) and a **global search** input. Each action drives an existing feature, so the toolbar is the ready-made
UI you would otherwise build with the [`toolbar` slot](https://coreui.io/data-grid/docs/features/slots/).
```js
new coreui.DataGrid(element, {
columns,
items,
columnVisibility: true, // required for the column chooser
toolbar: true, // every action whose feature is enabled
})
```
`toolbar: true` enables each action whose underlying feature is on: **columns**
needs [`columnVisibility`](https://coreui.io/data-grid/docs/columns/ordering-visibility/), **export** is always
available, **undo/redo** needs [`history`](https://coreui.io/data-grid/docs/features/history/), and **search**
turns on the global filter. Pass a granular object to
pick actions individually — `search: true` is the same input as
[`globalFilter: true`](https://coreui.io/data-grid/docs/features/filtering/), so keep using whichever reads
better.
```js
new coreui.DataGrid(element, {
columns,
items,
columnVisibility: true,
toolbar: {
columns: true, // column chooser popup
export: { filename: 'users.csv' }, // CsvDownloadOptions pass-through
history: true, // undo/redo buttons (needs history: true)
search: true, // global search input
},
})
```
```html
```
```js
const roles = ['admin', 'editor', 'viewer']
const items = Array.from({ length: 1000 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
role: roles[i % roles.length]
}))
new coreui.DataGrid(document.getElementById('dataGridToolbar'), {
columns: [
{
key: 'id', label: '#', width: 90, hideable: false
},
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email', style: { width: '30%' } },
{ key: 'role', label: 'Role', width: 110 }
],
items,
itemKey: item => String(item.id),
columnVisibility: true,
pagination: { pageSize: 10 },
toolbar: {
columns: true,
export: { filename: 'users.csv' },
search: true
}
})
```
## Column chooser
With `toolbar.columns` enabled the columns button opens a popup listing every
leaf column in visual order with a checkbox. Toggling a checkbox calls
`column.toggleVisibility()` live — there is no Apply step. Columns marked
`hideable: false` stay checked and disabled. The footer offers **Show all** and
**Reset** (Reset restores the initial `columnVisibility` object). Visibility
changes emit `visibilityChange.coreui.data-grid` just like the
[column menu](https://coreui.io/data-grid/docs/columns/menu/).
## Export
The export button downloads the current view as CSV by calling
[`downloadCsv({ scope: 'filtered' })`](https://coreui.io/data-grid/docs/features/csv-export/). Pass a
[`CsvDownloadOptions`](https://coreui.io/data-grid/docs/features/csv-export/) object as `toolbar.export` to set
`filename`, `delimiter`, `bom`, `sanitize` or `scope`.
## Icons
The toolbar buttons use the CoreUI icon set, overridable per-instance with the
`toolbarColumnsIcon`, `toolbarExportIcon`, `toolbarUndoIcon` and
`toolbarRedoIcon` string options (SVG markup, sanitized like every other
icon).
## Custom toolbar
The `toolbar` slot still **replaces** the whole toolbar; the built-in buttons are
not composable into a custom slot. For a fully custom toolbar — including a
hand-built column chooser — use the [`toolbar` slot](https://coreui.io/data-grid/docs/features/slots/) with the
headless `table` and public helpers (`downloadCsv`, `column.toggleVisibility`).
See the [column ordering & visibility](https://coreui.io/data-grid/docs/columns/ordering-visibility/) page for a
slot-based chooser.
---
# Data Grid Slots & Custom Rendering
> Replace the Data Grid's toolbar, pagination and empty-state chrome with your own markup through the slots factory API.
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`](https://coreui.io/data-grid/docs/columns/overview/)
instead.
## Custom slots
Replace the grid's chrome — `toolbar`, `pagination` and `empty` — with your own
markup. A slot is a factory `({ table, labels }) => ({ element, update?, dispose? })`:
`element` is mounted in place of the built-in module, `update()` runs on every
grid render (read state from the headless `table`), and `dispose()` runs on
teardown. This demo swaps the built-in pagination for a minimal Previous/Next
control driven entirely through `table`.
```html
```
```js
const roles = ['admin', 'editor', 'viewer']
const items = Array.from({ length: 1000 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
role: roles[i % roles.length]
}))
new coreui.DataGrid(document.getElementById('dataGridSlots'), {
columns: [
{ key: 'id', label: '#', width: 90 },
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email', style: { width: '30%' } },
{ key: 'role', label: 'Role', width: 110 }
],
items,
itemKey: item => String(item.id),
pagination: { pageSize: 10 },
slots: {
pagination({ table }) {
const element = document.createElement('div')
element.className = 'd-flex gap-2 align-items-center mt-2'
const prev = document.createElement('button')
prev.className = 'btn btn-sm btn-outline-secondary'
prev.textContent = 'Previous'
prev.addEventListener('click', () => table.previousPage())
const next = document.createElement('button')
next.className = 'btn btn-sm btn-outline-secondary'
next.textContent = 'Next'
next.addEventListener('click', () => table.nextPage())
const info = document.createElement('span')
info.className = 'text-body-secondary'
element.append(prev, info, next)
return {
element,
update() {
const { pageIndex } = table.store.state.pagination
info.textContent = `Page ${pageIndex + 1} of ${table.getPageCount()} · ${table.getRowCount()} items`
prev.disabled = !table.getCanPreviousPage()
next.disabled = !table.getCanNextPage()
}
}
}
}
})
```
Return a fresh `element` on every factory call — with `pagination.position: 'both'`
the factory runs 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` event to
tell the two apart.
## Slot contract
| Slot | Replaces | Signature |
| --- | --- | --- |
| `toolbar` | The toolbar above the grid | `({ table, labels }) => ({ element, update?, dispose? })` |
| `pagination` | The pagination bar | same |
| `empty` | The no-rows / load-error state | same |
- `element` — a DOM node mounted in place of the built-in module.
- `update()` — runs on every grid render; read current state from the headless
[`table`](https://coreui.io/data-grid/docs/api/headless/).
- `dispose()` — runs on teardown; clean up listeners here.
---
# Data Grid Print
> Print the whole Data Grid — every sorted and filtered row, not the twenty the virtualizer has on screen or the page the user happens to be on.
Printing a virtualized grid straight from the browser prints the twenty rows
that happen to be in the DOM, and a paginated one prints the current page.
Neither is what anyone means by "print this table", so the grid renders a
plain, complete table, prints that, and throws it away.
## Print
Enable the toolbar action with `toolbar: { print: true }`, or call `print()` on
the instance from your own button. Search the grid below, then print: the
printout carries every matching row, not just the visible ones.
```html
```
```js
new coreui.DataGrid(document.getElementById('dataGridPrint'), {
columns: [
{ key: 'name', label: 'Name' },
{ key: 'role', label: 'Role' },
{ key: 'country', label: 'Country' }
],
items: Array.from({ length: 60 }, (_, index) => ({
name: `Person ${index + 1}`,
role: index % 3 === 0 ? 'admin' : 'user',
country: ['Poland', 'Germany', 'Spain', 'Italy'][index % 4]
})),
printTitle: 'Team roster',
toolbar: { print: true, search: true },
globalFilter: true
})
```
## What gets printed
- **Every sorted and filtered row.** Pagination and virtualization are display
concerns; the printout is the whole result set the user's sorting, filters and
search produced.
- **The visible columns**, in their current order — hidden and reordered
columns follow what is on screen.
- **Values as displayed.** A column's `formatter` runs first, so a formatted
date prints as the date on screen.
- **The `printTitle`** as a heading above the table, when set.
Custom cell renderers are *not* carried into the printout: it is built from
values as text, never from markup, so the printed page can never become an
injection surface for the data.
## Options
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `toolbar.print` | `boolean` | `false` | Adds the print button to the built-in toolbar. |
| `printTitle` | `string \| null` | `null` | Heading printed above the table. |
| `toolbarPrintIcon` | `string` | printer | Custom print icon (SVG string). |
## Styling the printout
The clone is a `.data-grid-print-root` element appended to the document, and the
print stylesheet hides everything else on the page while it is there. Restyle it
like any other markup:
```css
@media print {
.data-grid-print-root h1 { font-size: 1rem; }
.data-grid-print-root :is(th, td) { border-color: #999; }
}
```
---
# Data Grid CSV Export
> Export Data Grid rows to an RFC-4180 CSV string or file, with scope, delimiter, BOM and formula-injection sanitization options.
Give users a one-click download of what they're looking at. `grid.getCsv()`
returns a spec-compliant CSV string and `grid.downloadCsv()` saves it as a file,
both respecting the grid's current layout and filters. The pure CSV helper also
ships standalone for server-side or headless use with no runtime dependencies.
## CSV export
`grid.getCsv(options)` returns an RFC-4180 string; `grid.downloadCsv(options)`
saves it as a file. Exported columns follow the rendered layout (pinning, order,
visibility). Values use each column's `formatter` (never `render`); `scope`
picks `'filtered'` (default, all matching rows), `'all'` (ignores filters) or
`'selected'`; `delimiter` and `bom` (Excel-friendly UTF-8) are configurable.
Set `sanitize: true` to guard against CSV formula injection (prefixes fields
starting with `=`, `+`, `-` or `@` with an apostrophe) when exporting untrusted
data. Server-side grids export the rows currently in memory. The pure helper is
also published at `@coreui/data-grid/csv` — no runtime dependencies — for use without
the component.
```html
Export CSV
```
```js
const roles = ['admin', 'editor', 'viewer']
const items = Array.from({ length: 1000 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
role: roles[i % roles.length]
}))
const grid = new coreui.DataGrid(document.getElementById('dataGridCsv'), {
columns: [
{ key: 'id', label: '#', width: 90 },
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email', style: { width: '30%' } },
{
key: 'role', label: 'Role', width: 110, formatter: value => String(value).toUpperCase()
}
],
items,
itemKey: item => String(item.id),
columnFilters: true,
pagination: { pageSize: 10 },
rowSelection: true
})
document.getElementById('dataGridCsvBtn').addEventListener('click', () => {
grid.downloadCsv({ filename: 'users.csv', scope: 'filtered', bom: true })
})
```
## Options
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `scope` | `'filtered' \| 'all' \| 'selected'` | `'filtered'` | Which rows to export. |
| `delimiter` | `string` | `','` | Field delimiter. |
| `bom` | `boolean` | `false` | Prepend a UTF-8 BOM for Excel. |
| `sanitize` | `boolean` | `false` | Prefix formula-triggering fields (`= + - @`) with `'` to prevent CSV injection. |
| `filename` | `string` | `'export.csv'` | `downloadCsv` only — the download filename. |
---
# Data Grid Excel Export
> Export Data Grid rows to a real .xlsx workbook — typed cells, a bold header row and column widths — from a dependency-free 3 KB subpath.
`@coreui/data-grid/xlsx` writes a real Excel workbook: numbers arrive as
numbers, the header row is bold, and declared column widths carry over. It is
a **separate subpath** with no runtime dependencies, so the writer only ships
to apps that import it — the main bundle stays untouched.
## Exporting
`exportXlsx(table, options)` returns the workbook as a `Uint8Array`;
`downloadXlsx(table, options)` saves it as a file. Both take the grid's
TanStack table — `grid.table` in the vanilla build:
```js
import { DataGrid } from '@coreui/data-grid'
import { downloadXlsx } from '@coreui/data-grid/xlsx'
const grid = new DataGrid('#grid', { columns, items })
document.querySelector('#export').addEventListener('click', () => {
downloadXlsx(grid.table, { filename: 'users.xlsx', sheetName: 'Users' })
})
```
Exported columns follow the rendered layout (pinning, order, visibility), and
`scope` picks `'filtered'` (the default — every row matching the current
filters, in the current sort order), `'all'` (ignores filters) or
`'selected'`. Server-side grids export the rows currently in memory.
## Toolbar button
The built-in toolbar export button writes CSV by default. Give it the Excel
writer through the `exporter` option — the button, its tooltip and its
placement stay the same:
```js
import { downloadXlsx } from '@coreui/data-grid/xlsx'
new DataGrid('#grid', {
columns,
items,
toolbar: { export: { exporter: downloadXlsx, filename: 'users.xlsx', sheetName: 'Users' } }
})
```
The writer is *injected* rather than imported by the grid, which is what keeps
it out of the main bundle. Any function with the same shape works, so this is
also the hook for a custom exporter (PDF, a server round-trip, your own
format).
## Cell types and formatting
| Grid value | Excel cell |
| --- | --- |
| `number` (finite) | numeric — sorts and sums in Excel |
| `boolean` | boolean |
| anything else | inline string |
| column has a `formatter` | the formatter's output, as text |
A column `formatter` is author intent and matches what the grid renders, so
the export honors it — which also means a formatted number leaves as text.
Drop the formatter on columns you want Excel to treat as numbers.
Cell text is never a formula: values are written as inline strings, so a
leading `=` stays literal data and no formula-injection guard is needed
(unlike [CSV export](https://coreui.io/data-grid/docs/features/csv-export/), where `sanitize` exists for
exactly that).
Column widths come from the column's declared `width` (px, converted to
Excel's character units). Columns without one keep Excel's default width;
an interactive resize is not carried into the export.
## Options
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `scope` | `'filtered' \| 'all' \| 'selected'` | `'filtered'` | Which rows to export. |
| `sheetName` | `string` | `'Sheet1'` | Worksheet name; invalid characters are replaced and the name is capped at Excel's 31-character limit. |
| `filename` | `string` | `'export.xlsx'` | `downloadXlsx` only. |
## Size
The subpath is around 3 KB gzipped and pulls in nothing else: the workbook is
assembled as SpreadsheetML XML inside a ZIP container written by hand. Nothing
is added to the grid bundle unless you import it.
---
# Data Grid Columns Overview
> Define CoreUI Data Grid columns — keys, labels, cheap value formatting with formatter, and rich cell content with render.
Columns are defined by the [`columns`](https://coreui.io/data-grid/docs/api/options/) array. Each entry maps a
`key` in your data to a header and a cell. This page covers the essentials;
per-column features live on their own pages:
[sizing](https://coreui.io/data-grid/docs/columns/sizing/), [pinning](https://coreui.io/data-grid/docs/columns/pinning/),
[ordering & visibility](https://coreui.io/data-grid/docs/columns/ordering-visibility/) and the
[column menu](https://coreui.io/data-grid/docs/columns/menu/).
## Defining columns
```js
new coreui.DataGrid(element, {
columns: [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Role' },
],
items,
})
```
`key` is the property read from each item and doubles as the column id. `label`
is the header text — it falls back to `key` when omitted.
## Formatting values
Use `formatter` to transform the displayed value. It's cheap and stays on the
scroll hot path, so it's the right tool for dates, numbers and currency:
```js
{
key: 'createdAt',
label: 'Created',
formatter: (value) => new Date(value).toLocaleDateString(),
}
```
`formatter` output is also what [CSV export](https://coreui.io/data-grid/docs/features/csv-export/) writes.
## Rich cell content
Use `render` for full custom cell content — action buttons, badges, links.
`render` returns a DOM node or HTML string and is **never** used for CSV export:
```js
{
key: 'actions',
label: '',
render: (item) => {
const button = document.createElement('button')
button.className = 'btn btn-sm btn-primary'
button.textContent = 'Edit'
button.addEventListener('click', () => edit(item))
return button
},
}
```
Use `formatter` **or** `render` per column — `formatter` for values on the hot
path, `render` for interactive content. See the [column API](https://coreui.io/data-grid/docs/api/columns/) for
every key.
---
# Data Grid Column Sizing
> Add drag-to-resize handles to Data Grid header cells, with live or on-release width updates and per-column opt-out.
Let users widen a column to read long values or shrink one they don't care
about. `columnSizing` adds a drag handle to every resizable header cell; widths
persist in the grid's state and can be committed live or on release.
## Column resizing
Set `columnSizing: true` to add a drag handle to the right edge of every header
cell. Widths follow the pointer live (`{ mode: 'onEnd' }` commits them on release
instead), the grid scrolls horizontally once the columns outgrow the viewport,
and `column.width` seeds the starting width. Opt a column out with
`resizable: false` — here the `#` column stays fixed.
```html
```
```js
const roles = ['admin', 'editor', 'viewer']
const items = Array.from({ length: 1000 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
role: roles[i % roles.length]
}))
new coreui.DataGrid(document.getElementById('dataGridSizing'), {
columns: [
{
key: 'id', label: '#', width: 90, resizable: false
},
{ key: 'name', label: 'Name', width: 220 },
{ key: 'email', label: 'Email', width: 280 },
{ key: 'role', label: 'Role', width: 160 }
],
items,
itemKey: item => String(item.id),
columnSizing: true, // or { mode: 'onEnd' } to commit widths on release
pagination: { pageSize: 10 }
})
```
## Options
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `columnSizing` | `boolean \| { mode?: 'onChange' \| 'onEnd' }` | `false` | Enables resize handles. `mode` controls whether widths update while dragging (`'onChange'`, the default) or on release (`'onEnd'`). |
| `resizable` (column) | `boolean` | `true` | Set `false` to drop the resize handle for a column. |
| `width` (column) | `number` | — | Seeds the starting width in pixels, e.g. `200`. |
Resizing emits `sizingChange.coreui.data-grid` with the grid's
`{ columnSizing }` state.
---
# Data Grid Column Pinning
> Freeze Data Grid columns to the start or end edge so they stay visible while the rest of the grid scrolls horizontally.
Keep an identifier or an actions column in view while users scroll a wide grid
sideways. Pinning freezes columns against the start or end edge with sticky
positioning; everything else scrolls between them.
## Column pinning
Freeze columns against the start or end edge with
`columnPinning: { start, end }` — they stay put while the rest of the grid
scrolls horizontally. Pass `columnPinning: true` to enable the feature and pin
later through the headless table (`grid.table.setColumnPinning(...)`). When a
column is pinned to the start and selection is on, the checkbox column travels
with it. The last start-pinned and first end-pinned column cast a shadow over
the scrolling content.
The regions are logical, not physical: `start` is the left edge in LTR and the
right edge in RTL (the sticky offsets use `inset-inline-start`/`inset-inline-end`).
Pinning moves the column to its edge — the rendered order is always
start-pinned, center, end-pinned.
```html
```
```js
const firstNames = ['Alice', 'Bob', 'Carol', 'Dave', 'Eve']
const lastNames = ['Smith', 'Jones', 'Brown', 'Taylor', 'Wilson']
const countries = ['Poland', 'Germany', 'France', 'Spain', 'Italy']
const items = Array.from({ length: 1000 }, (_, i) => ({
id: i + 1,
firstName: firstNames[i % firstNames.length],
lastName: lastNames[i % lastNames.length],
email: `user${i + 1}@example.com`,
country: countries[i % countries.length]
}))
new coreui.DataGrid(document.getElementById('dataGridPinning'), {
columns: [
{ key: 'id', label: '#', width: 80 },
{ key: 'firstName', label: 'First name', width: 160 },
{ key: 'lastName', label: 'Last name', width: 160 },
{ key: 'email', label: 'Email', width: 260 },
{ key: 'country', label: 'Country', width: 200 },
{
key: 'actions',
label: '',
sortable: false,
width: 120,
render(item) {
const button = document.createElement('button')
button.type = 'button'
button.className = 'btn btn-sm btn-outline-primary'
button.textContent = 'Edit'
button.addEventListener('click', () => alert(`Edit ${item.firstName} (#${item.id})`))
return button
}
}
],
items,
itemKey: item => String(item.id),
columnPinning: { start: ['id'], end: ['actions'] },
rowSelection: true,
pagination: { pageSize: 10 }
})
```
## Options
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `columnPinning` | `boolean \| { start?: string[], end?: string[] }` | `false` | Freezes columns (by `key`) to the start/end edge. `true` enables the feature with no initial pins. |
Pinning emits `pinningChange.coreui.data-grid` with the grid's
`{ columnPinning }` state. Reordering via
[column ordering](https://coreui.io/data-grid/docs/columns/ordering-visibility/) never crosses a pinning
boundary.
---
# Data Grid Column Ordering & Visibility
> Let users reorder Data Grid columns by drag-and-drop and hide or show columns through a column chooser.
Give users control over the layout: drag headers to reorder columns and toggle
columns on and off to focus on what matters. Both features are driven through
the headless table, so you can wire them into your own toolbar — as the column
chooser in this demo does.
## Column ordering & visibility
`columnOrder: true` makes headers draggable — drop one onto another to reorder
(dragging never crosses a pinning boundary); pass an array for an initial order.
`columnVisibility: true` enables hiding and showing columns through the headless
table (`column.toggleVisibility()`, `column.getIsVisible()`); pass an object like
`{ email: false }` to start with a column hidden. Opt a column out with
`movable: false` / `hideable: false`. For a ready-made chooser, enable the
built-in [toolbar](https://coreui.io/data-grid/docs/features/toolbar/) (`toolbar: { columns: true }`); this demo
instead builds one entirely with the `toolbar` slot to show the headless API.
```html
```
```js
const roles = ['admin', 'editor', 'viewer']
const items = Array.from({ length: 1000 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
role: roles[i % roles.length]
}))
new coreui.DataGrid(document.getElementById('dataGridOrderVisibility'), {
columns: [
{
key: 'id', label: '#', width: 90, movable: false, hideable: false
},
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email', style: { width: '30%' } },
{ key: 'role', label: 'Role', width: 110 }
],
items,
itemKey: item => String(item.id),
columnOrder: true,
columnVisibility: true,
pagination: { pageSize: 10 },
slots: {
toolbar({ table }) {
const element = document.createElement('div')
element.className = 'd-flex gap-3 mb-2'
const inputs = []
for (const column of table.getAllLeafColumns()) {
if (!column.getCanHide()) {
continue
}
const label = document.createElement('label')
label.className = 'form-check form-check-inline m-0'
const input = document.createElement('input')
input.type = 'checkbox'
input.className = 'form-check-input me-1'
input.addEventListener('change', () => column.toggleVisibility(input.checked))
label.append(input, column.id)
element.append(label)
inputs.push([column, input])
}
return {
element,
update() {
for (const [column, input] of inputs) {
input.checked = column.getIsVisible()
}
}
}
}
}
})
```
## The drag interaction
Dragging is pointer-based, not HTML5 drag-and-drop, which is what makes the
following possible at all — and makes it behave the same with a mouse, a pen and
a finger:
- **A ghost** follows the pointer with the column's label, and the source header
dims while it travels.
- **A live preview** — the columns the dragged one passes slide aside, and it
slides into the gap, so the grid shows the layout the drop will produce
instead of pointing at it with a line. A swap fires the moment the pointer
crosses into a neighbour — at its edge, not its midpoint — and the release
commits whatever the preview shows. Nothing reflows: it is `transform` only,
and the real order still changes once, on drop. Where a drop is not allowed
(another pinned region), nothing slides — and a release past either end of
the axis lands the column at that end.
- **Auto-scroll** at the viewport's edges, ramping with how far into the edge the
pointer sits, so a column can cross a grid wider than the screen.
- **The ghost settles** — on release it flies from wherever you let go to the
slot the column landed in and fades out there; on Escape it flies back home.
- **Escape cancels** mid-drag, and the click that follows a drag never toggles
the sort of the header it started from.
A press only becomes a drag after it travels a few pixels, so clicking a sortable
header still sorts it.
The slide honours `prefers-reduced-motion`: someone who asked the OS to stop
moving things still sees the preview, snapped to the final layout instead of
gliding there. Retime it with the `--cui-data-grid-drag-transition` token.
## Options
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `columnOrder` | `boolean \| string[]` | `false` | Drag-and-drop reordering; an array sets the initial order. Never crosses a pinning boundary. |
| `columnVisibility` | `boolean \| Record` | `false` | Enables hiding/showing columns; an object sets the initial visibility. |
| `movable` (column) | `boolean` | `true` | Set `false` to exclude a column from reordering. |
| `hideable` (column) | `boolean` | `true` | Set `false` to prevent hiding a column. |
Reordering emits `orderChange.coreui.data-grid` `{ columnOrder }`; toggling
visibility emits `visibilityChange.coreui.data-grid` `{ columnVisibility }`. The
[column menu](https://coreui.io/data-grid/docs/columns/menu/) offers a keyboard-accessible Move to start/end as an
alternative to drag-and-drop.
---
# Data Grid Column Menu
> Add a per-column header menu to the Data Grid that gathers sort, pin, move and hide actions behind one accessible ⋮ button, and customize its items with a builder.
The column menu collects a column's actions — sort, pin, move, hide — behind a
single ⋮ button in the header, so users don't have to discover drag-and-drop. It
is the keyboard-accessible path to sorting, reordering and pinning, and it only
shows the actions you've actually enabled.
## Column header menu
`columnMenu: true` adds a ⋮ button to each header cell with the column's
actions in one place — Sort ascending/descending/Unsort, Pin to start/end/Unpin,
Move to start/end and Hide column. Items appear only for enabled features
(`sorting`, `columnPinning`, `columnOrder`, `columnVisibility`) and respect
per-column `sortable`/`movable`/`hideable` opt-outs; a column with no available
action gets no button. Related actions are separated into groups by a divider.
The menu follows the ARIA menu pattern (arrow keys, Home/End, Escape restores
focus) — Move to start/end is the keyboard-accessible way to reorder columns,
complementing drag & drop. All labels are translatable via `labels`.
```html
```
```js
const roles = ['admin', 'editor', 'viewer']
const items = Array.from({ length: 1000 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
role: roles[i % roles.length]
}))
new coreui.DataGrid(document.getElementById('dataGridColumnMenu'), {
columns: [
{
key: 'id', label: '#', width: 90, movable: false, hideable: false
},
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email', style: { width: '30%' } },
{ key: 'role', label: 'Role', width: 140 }
],
items,
itemKey: item => String(item.id),
columnMenu: true,
columnOrder: true,
columnPinning: true,
columnVisibility: true,
pagination: { pageSize: 10 }
})
```
## Customize the menu
Pass a **builder function** to `columnMenu` to take full control of the items.
It receives the column and the built-in actions for that column, and returns the
final list — so you can reorder, drop or add items, per column:
```js
columnMenu: ({ column, defaultActions }) => DataGridMenuAction[]
```
- **Reorder** — return `defaultActions` in a different order.
- **Hide an item** — filter it out by `key` (`'sort-asc'`, `'sort-desc'`,
`'clear-sort'`, `'pin-start'`, `'pin-end'`, `'unpin'`, `'move-start'`,
`'move-end'`, `'hide'`).
- **Add your own** — push a new action object.
- **Per column** — branch on `column` (the [column definition](https://coreui.io/data-grid/docs/api/options/)).
A column whose builder returns at least one action shows the ⋮ button, even with
no built-in feature enabled. Each action has this shape:
```js
{
key: string, // unique id, also used to filter built-ins
label: string, // menu item text
icon?: string, // SVG markup, sanitized before insertion
disabled?: boolean, // render but don't run
group?: string, // items with different groups get a divider between them
run: () => void // invoked on click; the menu closes first
}
```
Icons are sanitized against the SVG allow list, like every other Data Grid icon —
see [icons](https://coreui.io/data-grid/docs/customization/styling/#icons). Set `sanitize: false` to opt out, or
pass a `sanitizeFn` to plug in your own sanitizer.
```html
```
```js
const roles = ['admin', 'editor', 'viewer']
const items = Array.from({ length: 1000 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
role: roles[i % roles.length]
}))
const copyIcon =
' '
new coreui.DataGrid(document.getElementById('dataGridColumnMenuCustom'), {
columns: [
{
key: 'id', label: '#', width: 90, movable: false, hideable: false
},
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email', style: { width: '30%' } },
{ key: 'role', label: 'Role', width: 140 }
],
items,
itemKey: item => String(item.id),
// A builder receives the built-in actions plus the column, and returns the
// final list — filter items, reorder them, or add your own.
columnMenu: ({ column, defaultActions }) => [
...defaultActions,
{
key: 'copy-header',
label: 'Copy header',
group: 'custom',
icon: copyIcon,
run: () => navigator.clipboard?.writeText(column.label ?? column.key)
}
],
columnOrder: true,
columnPinning: true,
columnVisibility: true,
pagination: { pageSize: 10 }
})
```
## Options
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `columnMenu` | `boolean` \| `(context) => DataGridMenuAction[]` | `false` | Adds a per-column header menu. `true` builds it from the enabled features; a builder function returns the final action list (see [Customize the menu](#customize-the-menu)). |
| `sanitize` | `boolean` | `true` | Sanitize icon markup (menu items and header icons) against the SVG allow list. |
| `sanitizeFn` | `function` \| `null` | `null` | Custom sanitizer used in place of the built-in one. |
The default menu's items depend on which features are on
([sorting](https://coreui.io/data-grid/docs/features/sorting/), [pinning](https://coreui.io/data-grid/docs/columns/pinning/),
[ordering & visibility](https://coreui.io/data-grid/docs/columns/ordering-visibility/)). Its labels come from
[`labels`](https://coreui.io/data-grid/docs/customization/localization/); see
[Accessibility](https://coreui.io/data-grid/docs/guides/accessibility/) for the keyboard model.
---
# Data Grid Styling & Theming
> Theme the CoreUI Data Grid with --cui-data-grid-* CSS variables that resolve through CoreUI semantic tokens, so light and dark modes work with no extra CSS.
The Data Grid is styled entirely with CSS custom properties. Every knob is a
`--cui-data-grid-*` variable that resolves through CoreUI's semantic tokens
(`--cui-body-bg`, `--cui-border-color`, …), so the grid inherits your theme —
including `data-coreui-theme="dark"` — with no extra rules.
## Light & dark mode
Because the tokens resolve through CoreUI semantic variables, dark mode works out
of the box:
```html
```
No grid-specific dark styles are needed — the semantic tokens flip and the grid
follows.
## Overriding tokens
Set any variable on the grid element (or an ancestor) to retheme it:
```css
#grid {
--cui-data-grid-spacing: 0.75rem;
--cui-data-grid-header-bg: var(--cui-tertiary-bg);
--cui-data-grid-viewport-max-height: 40rem;
}
```
## CSS variables
| Variable | Default | Controls |
| --- | --- | --- |
| `--cui-data-grid-viewport-max-height` | `30rem` | Max height of the scroll viewport — the usual way to size the grid. Setting a `height` on the grid element works too: the viewport takes whatever the toolbar and pagination leave. |
| `--cui-data-grid-header-bg` | `var(--cui-body-bg, #fff)` | Header row background. |
| `--cui-data-grid-header-shadow` | `inset 0 calc(-1 * var(--cui-border-width, 1px)) 0 var(--cui-border-color, #dee2e6)` | Header bottom border. |
| `--cui-data-grid-cell-selected-bg` | `rgba(var(--cui-primary-rgb, 50, 31, 219), .12)` | Fill of a selected cell. |
| `--cui-data-grid-cell-selected-border-color` | `var(--cui-primary, #321fdb)` | Outline drawn on the edges of a selected range. |
| `--cui-data-grid-cell-selected-border-width` | `1px` | Width of that outline. |
| `--cui-data-grid-row-handle-width` | `2rem` | Width of the row-reorder handle column. |
| `--cui-data-grid-select-cell-width` | `2.5rem` | Width of the selection checkbox column. |
| `--cui-data-grid-sorter-margin` | `.25rem` | Gap before the sort indicator. |
| `--cui-data-grid-sorter-opacity` | `.3` | Resting opacity of the sort indicator. |
| `--cui-data-grid-spacing` | `.5rem` | Padding and gap of the toolbar and pagination bars. |
| `--cui-data-grid-resizer-width` | `.625rem` | Hit area of the resize handle. |
| `--cui-data-grid-resizer-grip-height` | `60%` | Height of the resize grip. |
| `--cui-data-grid-resizer-grip-color` | `var(--cui-border-color, #dee2e6)` | Resize grip color. |
| `--cui-data-grid-resizer-grip-color-hover` | `var(--cui-secondary-color, #6c757d)` | Resize grip color on hover. |
| `--cui-data-grid-pinned-border-width` | `var(--cui-border-width)` | Width of the divider at the pinned-column edge. |
| `--cui-data-grid-pinned-border-color` | `var(--cui-border-color, #dee2e6)` | Color of that divider. |
| `--cui-data-grid-pinned-bg` | `var(--cui-body-bg, #fff)` | Background of pinned cells. |
| `--cui-data-grid-drop-indicator` | `var(--cui-primary, #321fdb)` | Accent of the drag ghost while reordering (and the indicator line shown when the live preview is off). |
| `--cui-data-grid-drag-transition` | `.15s ease` | Timing of the live reorder preview and the settling ghost. Ignored under `prefers-reduced-motion`. |
| `--cui-data-grid-menu-bg` | `var(--cui-body-bg, #fff)` | Column menu background. |
| `--cui-data-grid-menu-border-color` | `var(--cui-border-color, #dee2e6)` | Column menu border. |
| `--cui-data-grid-menu-shadow` | `var(--cui-box-shadow, 0 .5rem 1rem rgba(0, 0, 0, .15))` | Column menu shadow. |
| `--cui-data-grid-menu-min-width` | `10rem` | Column menu minimum width. |
| `--cui-data-grid-menu-item-hover-bg` | `var(--cui-tertiary-bg, #f8f9fa)` | Column menu item hover background. |
| `--cui-data-grid-loading-bg` | `rgba(var(--cui-body-bg-rgb, 255, 255, 255), .6)` | Background of the loading overlay (server-side mode). |
| `--cui-data-grid-focus-ring-width` | `2px` | Width of the focus ring on the active cell. |
| `--cui-data-grid-focus-ring-color` | `var(--cui-primary, #321fdb)` | Color of that focus ring. |
The grid also inherits the `.table` styles (striping, borders) from
`@coreui/coreui(-pro)`, so table-level CSS variables like
`--cui-table-striped-bg` apply too.
## Sass
The stylesheet is a Sass module, so the same knobs are available at build time.
Every CSS variable above is backed by a Sass variable of the same name without
the `--cui-` prefix, and configuring one changes the emitted token:
```scss
@use "@coreui/data-grid/scss/data-grid" with (
$data-grid-spacing: .75rem,
$data-grid-viewport-max-height: 40rem
);
```
A token map is available too. It merges over the defaults, so you can add or
retarget individual custom properties — and a `null` value removes one:
```scss
@use "@coreui/data-grid/scss/data-grid" with (
$data-grid-tokens: (
--cui-data-grid-header-bg: var(--cui-tertiary-bg),
--cui-data-grid-menu-shadow: none
)
);
```
A few layout values have no runtime token and are configurable at build time
only — the toolbar/header bar height, the popup max-width, and the shared
border, radius and muted-color references the grid chrome reuses. They are the
`data-grid-layout-variables` block in `scss/components/_data-grid.scss`.
`$prefix` is configurable as well (`cui-` by default), for the case where the
surrounding CoreUI build uses a different one.
## Without the CoreUI stylesheet
The grid's markup uses `.table`, `.btn`, the form controls, `.pagination`,
`.spinner-border` and `.visually-hidden` from `@coreui/coreui(-pro)`. If that
stylesheet is not on the page, load `data-grid.standalone.css` instead of
`data-grid.css` — it carries those base styles itself for ~2.7 kB gzip. See
[Installation](https://coreui.io/data-grid/docs/getting-started/installation/).
Two properties make it safe to drop in:
- every base rule is **scoped to `.data-grid`**, so the sheet never restyles the
rest of your page;
- every base rule sits in the **`data-grid-base` cascade layer**, while the grid's
own rules stay unlayered. A layered declaration always loses to an unlayered
one, so if CoreUI (or your own CSS) is loaded after all, it wins on every
property it declares and the two can coexist.
That second point assumes the surrounding stylesheet is unlayered, which is true
of CoreUI 5. Against a stylesheet that uses cascade layers itself, the outcome
becomes ordinary layer order — rename the layer to place it where you need:
```scss
@use "@coreui/data-grid/scss/data-grid.standalone" with (
$base-layer: vendor
);
```
The base styles are configurable the same way as the grid. Their variables are
prefixed `$base-` and cover the palette, the control sizing and the icons:
```scss
@use "@coreui/data-grid/scss/data-grid.standalone" with (
$base-primary: #0d6efd,
$base-border-color: #d0d7de,
$base-btn-font-size-sm: .8125rem
);
```
---
# Data Grid Localization
> Translate every CoreUI Data Grid UI string through the labels option, with {token} interpolation for dynamic values.
Every UI string the grid renders — menu items, pagination labels, ARIA
announcements — comes from the `labels` option. Pass your own strings and they're
merged over the defaults, so you only override what you need.
## Usage
```js
new coreui.DataGrid(element, {
columns,
items,
labels: {
globalFilterPlaceholder: 'Szukaj…',
pageSizeLabel: 'Wierszy na stronę',
itemsInfo: '{first}–{last} z {total}',
},
})
```
## Interpolation
Labels with `{token}` placeholders are interpolated at render time — unknown
tokens are left untouched. Available tokens: `{column}` (column label),
`{first}`, `{last}`, `{total}` (pagination range) and `{count}` (results
announcement).
## Default labels
| Key | Default | Used by |
| --- | --- | --- |
| `addCondition` | `Add condition` | [Filter menu](https://coreui.io/data-grid/docs/features/filtering/) |
| `applyFilter` | `Apply` | [Filter menu](https://coreui.io/data-grid/docs/features/filtering/) |
| `clearFilter` | `Clear filter` | [Filter menu](https://coreui.io/data-grid/docs/features/filtering/) + quick-input clear |
| `clearSort` | `Unsort` | [Column menu](https://coreui.io/data-grid/docs/columns/menu/) sort actions |
| `columnMenu` | `Column options for {column}` | [Column menu](https://coreui.io/data-grid/docs/columns/menu/) button |
| `filterAction` | `Filter…` | [Column menu](https://coreui.io/data-grid/docs/columns/menu/) action |
| `filterColumn` | `Filter {column}` | [Filter](https://coreui.io/data-grid/docs/features/filtering/) input |
| `filterSummaryConditions` | `{count} conditions` | Quick-input summary |
| `filterSummarySelected` | `{count} selected` | Quick-input summary |
| `firstPage` | `First page` | [Pagination](https://coreui.io/data-grid/docs/features/pagination/) |
| `globalFilterLabel` | `Search` | Global search accessible label |
| `globalFilterPlaceholder` | `Search` | Global search placeholder |
| `hideColumn` | `Hide column` | Column menu |
| `itemsInfo` | `{first}–{last} of {total}` | Pagination range summary |
| `joinAnd` | `AND` | [Filter menu](https://coreui.io/data-grid/docs/features/filtering/) |
| `joinOr` | `OR` | [Filter menu](https://coreui.io/data-grid/docs/features/filtering/) |
| `lastPage` | `Last page` | Pagination |
| `loadError` | `Failed to load data` | [Server-side](https://coreui.io/data-grid/docs/features/server-side-data/) error state |
| `loading` | `Loading…` | Server-side loading overlay |
| `moveEnd` | `Move to end` | Column menu |
| `moveStart` | `Move to start` | Column menu |
| `nextPage` | `Next page` | Pagination |
| `operatorBetween` | `Between` | Filter menu operators |
| `operatorBlank` | `Blank` | Filter menu operators |
| `operatorContains` | `Contains` | Filter menu operators |
| `operatorEndsWith` | `Ends with` | Filter menu operators |
| `operatorEquals` | `Equals` | Filter menu operators |
| `operatorGreaterThan` | `Greater than` | Filter menu operators |
| `operatorGreaterThanOrEqual` | `Greater than or equal` | Filter menu operators |
| `operatorLessThan` | `Less than` | Filter menu operators |
| `operatorLessThanOrEqual` | `Less than or equal` | Filter menu operators |
| `operatorNotBlank` | `Not blank` | Filter menu operators |
| `operatorNotContains` | `Does not contain` | Filter menu operators |
| `operatorNotEquals` | `Not equals` | Filter menu operators |
| `operatorStartsWith` | `Starts with` | Filter menu operators |
| `pageSizeLabel` | `Rows per page` | Pagination page-size selector |
| `paginationLabel` | `Pagination` | Pagination nav accessible label |
| `pinEnd` | `Pin to end` | Column menu |
| `pinStart` | `Pin to start` | Column menu |
| `previousPage` | `Previous page` | Pagination |
| `redoneAnnouncement` | `Change redone` | [Undo & redo](https://coreui.io/data-grid/docs/features/history/) ARIA live announcement |
| `reorderRow` | `Reorder row` | [Row reordering](https://coreui.io/data-grid/docs/features/row-reordering/) drag-handle label |
| `resetColumns` | `Reset` | [Toolbar](https://coreui.io/data-grid/docs/features/toolbar/) column chooser |
| `resultsAnnouncement` | `{count} results` | ARIA live announcement |
| `searchValues` | `Search values` | Set filter search box |
| `selectAllRows` | `Select all rows` | [Selection](https://coreui.io/data-grid/docs/features/row-selection/) header checkbox |
| `selectAllValues` | `Select all` | Set filter |
| `selectRow` | `Select row` | Selection row checkbox |
| `showAllColumns` | `Show all` | [Toolbar](https://coreui.io/data-grid/docs/features/toolbar/) column chooser |
| `sortAscending` | `Sort ascending` | [Column menu](https://coreui.io/data-grid/docs/columns/menu/) sort actions |
| `sortDescending` | `Sort descending` | [Column menu](https://coreui.io/data-grid/docs/columns/menu/) sort actions |
| `toolbarColumns` | `Columns` | [Toolbar](https://coreui.io/data-grid/docs/features/toolbar/) columns button |
| `toolbarExport` | `Export` | [Toolbar](https://coreui.io/data-grid/docs/features/toolbar/) export button |
| `toolbarPrint` | `Print` | [Print](https://coreui.io/data-grid/docs/features/print/) toolbar button |
| `toolbarRedo` | `Redo` | [Toolbar](https://coreui.io/data-grid/docs/features/toolbar/) redo button |
| `toolbarUndo` | `Undo` | [Toolbar](https://coreui.io/data-grid/docs/features/toolbar/) undo button |
| `undoneAnnouncement` | `Change undone` | [Undo & redo](https://coreui.io/data-grid/docs/features/history/) ARIA live announcement |
| `unpin` | `Unpin` | Column menu |
The defaults are exported as `DEFAULT_LABELS` from `@coreui/data-grid` if you
want to extend rather than replace them.
---
# Data Grid Feature Sets & Bundle Size
> Pick the TanStack feature set your grid actually uses — the lite build covers sorting, filtering and pagination at 52 KB gzipped, the full build everything at 63 KB.
The grid registers a set of TanStack Table features at construction. Options
gate *behavior*; the feature set gates *registration* — an API only exists on
the table when its feature is registered, and unregistered features cost
nothing at runtime. Two presets ship with the package:
| Preset | Covers | Bundle |
| --- | --- | --- |
| `dataGridFeatures` (default) | everything: sorting, filtering, faceted set filters, global search, pagination, selection, visibility, ordering, pinning, sizing/resizing | `data-grid.min.js` — 55,534 B gzip |
| `dataGridLiteFeatures` | sorting, filtering (incl. the toolbar search) and pagination | `data-grid.lite.min.js` — 48,440 B gzip |
A grid that only sorts, filters and pages does not need the default build.
The lite bundle pairs the same `DataGrid` with `dataGridLiteFeatures` as its
default set and never references the rest:
```js
import { DataGrid } from '@coreui/data-grid/lite'
const grid = new DataGrid('#grid', {
columns,
items,
pagination: { pageSize: 20 },
toolbar: { search: true }
})
```
Or as a script tag: `dist/js/data-grid.lite.min.js` exposes the same
`coreui.DataGrid` global.
The main build also accepts the preset per instance — useful when one page
mixes full and lite grids (the bundle then carries the full set either way):
```js
import { DataGrid, dataGridLiteFeatures } from '@coreui/data-grid'
const grid = new DataGrid('#grid', { columns, items, features: dataGridLiteFeatures })
```
## What a lite grid can and cannot do
Everything driven by sorting, column filters, the global search and
pagination works exactly like the full build — the lite set keeps the same
sort and filter functions. Options whose feature is missing from the set
throw at construction with the pair spelled out, instead of failing somewhere
mid-render:
```
DATA-GRID: Option "columnPinning" requires feature "columnPinningFeature" in "features".
```
That covers `columnPinning`, `columnSizing`, `columnVisibility`,
`columnOrder`, `rowSelection` and `filterType: 'select'` columns (the faceted
value list needs faceting). CSV export and [state persistence](https://coreui.io/data-grid/docs/features/state/)
degrade gracefully: the export follows whatever layout the registered
features track, and a state snapshot taken on one feature set restores into a
grid running another — absent slices are skipped.
The framework bindings expose the same choice as a `features` prop/input and
get the size win from the app's own bundler instead of a second file — see
the React, Vue and Angular editions of this page.
---
# Data Grid Accessibility
> How CoreUI Data Grid supports keyboard interaction, ARIA roles and screen-reader announcements, and where the current limits are.
The Data Grid ships accessible controls for its interactive chrome and exposes
every string for translation. This page documents what's supported today and
what's still on the [roadmap](https://coreui.io/data-grid/docs/resources/roadmap/).
## Grid keyboard navigation
With [`cellNavigation`](https://coreui.io/data-grid/docs/features/keyboard-navigation/) on, the grid implements
the full ARIA [grid pattern](https://www.w3.org/WAI/ARIA/apg/patterns/grid/):
`role="grid"`, `aria-colcount`/`aria-colindex` alongside the absolute
`aria-rowcount`/`aria-rowindex`, a single tab stop with a roving-tabindex
active cell, and arrow-key movement across header and data cells. The option is
opt-in — claiming `role="grid"` without the full keyboard contract would be
worse than the native table semantics the grid keeps by default.
## Column menu
The [column menu](https://coreui.io/data-grid/docs/columns/menu/) follows the ARIA menu pattern:
- **Arrow keys** move between items; **Home/End** jump to the first/last item.
- **Escape** closes the menu and restores focus to the ⋮ trigger.
- **Move to start / Move to end** give a keyboard-accessible alternative to
drag-and-drop [column reordering](https://coreui.io/data-grid/docs/columns/ordering-visibility/).
All menu labels come from [`labels`](https://coreui.io/data-grid/docs/customization/localization/), so the menu
is fully translatable.
## Selection
The [selection](https://coreui.io/data-grid/docs/features/row-selection/) checkboxes are real form controls with
accessible labels (`selectRow`, `selectAllRows`) — reachable and toggleable by
keyboard.
## Server-side loading
In [server-side mode](https://coreui.io/data-grid/docs/features/server-side-data/) the grid sets `aria-busy`
while a request is in flight and exposes the `loading` label, so assistive tech
announces the pending state.
## Live announcements
Result counts are announced through an ARIA live region using the
`resultsAnnouncement` label (`{count} results`) as filters change.
## Current limits
`cellNavigation` (and the [inline editing](https://coreui.io/data-grid/docs/features/editing/) built on it) is
opt-in in this release. Screen-reader-verified defaults — flipping
`role="grid"` on out of the box — are a separate decision planned after NVDA
and VoiceOver testing.
---
# Data Grid Performance
> How CoreUI Data Grid stays fast at 100,000 rows, the options that tune rendering, and when to move paging to your backend.
The Data Grid is built to stay responsive on large datasets. This page explains
how it does that and the levers you have when you need to tune it.
## Why it's fast
- **Row virtualization.** Only the rows in the scroll viewport (plus a buffer)
exist in the DOM — see [Virtualization](https://coreui.io/data-grid/docs/features/virtualization/). A 100,000-row
grid renders a few dozen `` elements, not 100,000.
- **Cheap cell values.** Column [`formatter`](https://coreui.io/data-grid/docs/columns/overview/) runs on the
scroll hot path and returns a string, so it stays cheap. Reserve `render`
(which builds DOM) for the columns that truly need interactive content.
- **Debounced, race-safe fetches.** In
[server-side mode](https://coreui.io/data-grid/docs/features/server-side-data/), rapid state changes coalesce
into one request and stale responses are dropped.
## Tuning levers
| Lever | Effect |
| --- | --- |
| `rowHeight` | The estimated row height (default `44`). Set it close to your real height so the virtualizer sizes the scroll area accurately. |
| `overscan` | Extra rows rendered above/below the viewport (default `10`). Raise it if you see blank rows during fast scrolling; lower it to shave DOM nodes. |
| `formatter` vs `render` | Prefer `formatter` for values on the hot path; `render` allocates DOM per visible cell. |
## When to go server-side
Client-side mode keeps the whole dataset in memory. That's fine for tens of
thousands of rows, but past what the browser can hold — or when the data lives in
a database you don't want to ship whole — move sorting, filtering and paging to
your API with [server-side data](https://coreui.io/data-grid/docs/features/server-side-data/). The grid then
holds only the current page.
## Rules of thumb
- A few thousand to ~100k rows in the browser → client-side with virtualization.
- Beyond that, or data behind an API → server-side mode.
- Keep `formatter` pure and cheap; it runs for every visible cell on every
render.
---
# Data Grid RTL Support
> How CoreUI Data Grid behaves in right-to-left layouts — column order, reordering, resizing, keyboard direction and logical-property styling.
The Data Grid works in right-to-left layouts. Set `dir="rtl"` on the grid or on
any ancestor — the grid reads its own computed direction, so a single RTL
subtree inside an LTR page works as well as a whole RTL document.
```html
```
Nothing else is required: there is no RTL option, no separate stylesheet and no
mirrored build.
## What follows the writing direction
- **Column layout.** The first column sits on the right, and every logical
position follows from there. Pinned columns pin to the inline edges, so
`start` is the right edge and `end` the left one.
- **Column reordering.** Dragging a header runs on a logical axis, so the live
preview, the drop slot and the emitted `orderChange` describe positions in
column order, not in pixels. The same gesture produces the same result it
would in LTR, travelling the other way on screen.
- **Column resizing.** The handle sits on each column's inline end, which under
RTL is its left edge. Dragging it outward widens the column.
- **Row reordering.** Unaffected: rows move on the vertical axis.
- **Keyboard navigation.** Arrow keys are physical, column order is logical, so
the two horizontal keys trade places: → moves toward the first
column and ← toward the last. Shift+arrow grows a cell selection
the same way. Vertical keys, Home /End and
PageUp /PageDown are unchanged.
- **Chrome.** The toolbar, pagination, column menu, filter dialogs and the
column chooser lay out from the inline start and stay inside the grid.
## Exports and print
CSV, Excel and print output carry values, not layout, so they are written in
logical column order regardless of direction. A spreadsheet opening the file
applies its own direction.
## Switching direction at runtime
Flipping `dir` on a live grid is supported — an app with a language switcher
does not need to re-create the grid. The direction is re-read where it matters:
at the start of each drag and each resize.
## Styling
The stylesheet uses logical properties throughout (`inset-inline-*`,
`margin-inline-*`, `text-align: start`), so custom CSS built on the
[design tokens](https://coreui.io/data-grid/docs/customization/styling/) mirrors with it. Reach for logical
properties in your own overrides too — a hard-coded `left` or `margin-right`
is what breaks an otherwise direction-agnostic grid.
---
# Data Grid Options
> Full reference of CoreUI Data Grid constructor options — columns, data, features and behavior.
Pass options to the constructor: `new DataGrid(element, options)`. Update them
later with `grid.update(options)`.
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `autoRowHeight` | `boolean` | `false` | Measures each rendered row instead of trusting `rowHeight`, so rows grow with their content. Requires virtualization. See [Auto row height](https://coreui.io/data-grid/docs/features/virtualization/#auto-row-height). |
| `cellNavigation` | `boolean` | `false` | APG grid keyboard navigation — `role="grid"`, a roving-tabindex active cell and arrow-key movement. See [Keyboard navigation](https://coreui.io/data-grid/docs/features/keyboard-navigation/). |
| `cellSelection` | `boolean` | `false` | Spreadsheet-style cell ranges with clipboard copy; implies `cellNavigation`. See [Cell selection](https://coreui.io/data-grid/docs/features/cell-selection/). |
| `columns` | `DataGridColumn[]` | `[]` | Column definitions — see [Columns](https://coreui.io/data-grid/docs/api/columns/). |
| `columnFilters` | `boolean` | `false` | Renders a filter row in the header with an input per filterable column. See [Filtering](https://coreui.io/data-grid/docs/features/filtering/). |
| `columnMenu` | `boolean \| ((ctx) => DataGridMenuAction[])` | `false` | Adds a per-column header menu. `true` builds it from the enabled features (sort/pin/move/hide); a builder `({ column, defaultActions }) => actions` returns the final item list. See [Column menu](https://coreui.io/data-grid/docs/columns/menu/). |
| `columnOrder` | `boolean \| string[]` | `false` | Drag-and-drop column reordering; an array sets the initial order. Reordering never crosses a pinning boundary. See [Column ordering](https://coreui.io/data-grid/docs/columns/ordering-visibility/). |
| `columnPinning` | `boolean \| { start?: string[], end?: string[] }` | `false` | Freezes columns (by `key`) to the left/right edge with sticky positioning. `true` enables the feature with no initial pins. See [Column pinning](https://coreui.io/data-grid/docs/columns/pinning/). |
| `columnVisibility` | `boolean \| Record` | `false` | Enables hiding/showing columns via `column.toggleVisibility()`; an object sets the initial visibility. |
| `columnSizing` | `boolean \| { mode?: 'onChange' \| 'onEnd' }` | `false` | Enables column resize handles. `mode` controls whether widths update while dragging (`'onChange'`, the default) or on release (`'onEnd'`). See [Column sizing](https://coreui.io/data-grid/docs/columns/sizing/). |
| `dataProvider` | `(request) => Promise<{ items, totalRows }>` | `null` | Server-side mode: the grid requests data on every sorting/filter/page change. The request carries `{ sorting, columnFilters, globalFilter, pagination }`. Implies pagination. See [Server-side data](https://coreui.io/data-grid/docs/features/server-side-data/). |
| `editing` | `boolean` | `false` | Inline cell editing on Enter/F2/double-click for columns opting in via `editable`/`editor`; implies `cellNavigation`. See [Inline editing](https://coreui.io/data-grid/docs/features/editing/). |
| `empty` | `string \| () => Node \| string` | `'No items found'` | Content shown when no rows match. |
| `features` | `TableFeatures \| null` | `null` | The registered TanStack feature set; `null` means the entry's default — the full `dataGridFeatures` in the main build, `dataGridLiteFeatures` in `@coreui/data-grid/lite`. An option whose feature is missing from the set throws at construction. See [Feature sets](https://coreui.io/data-grid/docs/customization/feature-sets/). |
| `globalFilter` | `boolean` | `false` | Renders a search input above the grid that filters across all columns. Shorthand for `toolbar: { search: true }`. |
| `history` | `boolean` | `false` | Undo/redo stack over edit commits — toolbar buttons and Ctrl+Z / Ctrl+Shift+Z / Ctrl+Y. See [Undo & redo](https://coreui.io/data-grid/docs/features/history/). |
| `infiniteScroll` | `boolean \| { pageSize?, threshold? }` | `false` | Appends each loaded page to the rows already in view instead of replacing them. Requires `dataProvider` and `virtualization`; cannot be combined with `pagination`. See [Infinite scroll](https://coreui.io/data-grid/docs/features/infinite-scroll/). |
| `itemKey` | `(item, index) => string` | `null` | Stable row id — required for selection to survive sorting and filtering. |
| `items` | `object[]` | `[]` | Row data. |
| `labels` | `Partial` | `{}` | UI strings (i18n), merged over the defaults. Supports `{token}` interpolation. See [Localization](https://coreui.io/data-grid/docs/customization/localization/). |
| `overscan` | `number` | `10` | Extra rows rendered above and below the visible window. |
| `pagination` | `boolean \| { pageSize?, pageSizeOptions?, position?, info? }` | `false` | Pagination mode — mutually exclusive with virtualization. See [Pagination](https://coreui.io/data-grid/docs/features/pagination/). |
| `rowHeight` | `number` | `44` | Estimated row height in px used by the virtualizer. |
| `printTitle` | `string \| null` | `null` | Heading printed above the table when `print()` runs. See [Print](https://coreui.io/data-grid/docs/features/print/). |
| `rowHandleIcon` | `string` | grip icon | Custom row drag-handle icon (SVG string). |
| `rowOrder` | `boolean` | `false` | Adds a drag handle per row for reordering; emits `rowOrderChange` with the reordered array. See [Row reordering](https://coreui.io/data-grid/docs/features/row-reordering/). |
| `rowSelection` | `boolean \| { selectAll? }` | `false` | Checkbox selection with select-all and shift+click ranges. See [Row selection](https://coreui.io/data-grid/docs/features/row-selection/). |
| `sanitize` | `boolean` | `true` | Sanitize icon markup against the SVG allow list before inserting it. |
| `sanitizeFn` | `(html) => string \| null` | `null` | Custom sanitizer used in place of the built-in one. |
| `slots` | `{ toolbar?, pagination?, empty? }` | `{}` | Replaces built-in chrome. Each slot is a factory `({ table, labels }) => ({ element, update?, dispose? })`. See [Slots](https://coreui.io/data-grid/docs/features/slots/). |
| `stateKey` | `string \| null` | `null` | Autosaves the grid state to localStorage under this key (debounced) and restores it on init. See [Save & restore state](https://coreui.io/data-grid/docs/features/state/). |
| `sorting` | `boolean \| { multiple?, resetable? }` | `true` | Column sorting; shift+click adds columns to the sort. See [Sorting](https://coreui.io/data-grid/docs/features/sorting/). |
| `toolbar` | `boolean \| { columns?, export?, history?, search? }` | `false` | Built-in toolbar above the grid. `true` enables every action whose feature is on; a granular object picks each. `columns` needs `columnVisibility`; `export` accepts a `CsvDownloadOptions` object; `print` adds the [print](https://coreui.io/data-grid/docs/features/print/) button; `history` needs the `history` option; `search` is the same input as `globalFilter`. See [Toolbar](https://coreui.io/data-grid/docs/features/toolbar/). |
| `virtualization` | `boolean` | `true` | Windowed rendering for large datasets. See [Virtualization](https://coreui.io/data-grid/docs/features/virtualization/). |
Every menu and header icon is overridable with its own `string` option (SVG
markup): `columnMenuIcon`, `sortAscendingIcon`, `sortDescendingIcon`,
`sortNeutralIcon`, `pinStartIcon`, `pinEndIcon`, `unpinIcon`, `moveStartIcon`,
`moveEndIcon`, `hideColumnIcon`, `toolbarColumnsIcon`, `toolbarExportIcon`,
`toolbarUndoIcon` and `toolbarRedoIcon`.
They default to the CoreUI icon set and are sanitized like any other icon.
---
# Data Grid Column API
> Full reference of CoreUI Data Grid column definition keys — labels, sorting, filtering, formatting and custom rendering.
Each entry in the [`columns`](https://coreui.io/data-grid/docs/api/options/) array describes one column. `key` is
the only required field. See [Columns overview](https://coreui.io/data-grid/docs/columns/overview/) for a guided
tour.
| Key | Type | Description |
| --- | --- | --- |
| `key` | `string` | Property name in the item object (also the column id). |
| `label` | `string` | Header label; falls back to `key`. |
| `editable` | `boolean \| { type?, min?, max?, step?, options? }` | Opts the column into [inline editing](https://coreui.io/data-grid/docs/features/editing/) with a built-in `text`, `number` or `select` editor. |
| `editor` | `(context) => DataGridEditor` | Custom editor factory — receives `{ item, column, value, commit, cancel, labels }`, returns `{ element, getValue?, focus?, contains?, dispose?, popup? }`. Its presence opts the column in. |
| `editValidate` | `(value, item) => true \| string` | Gates the commit — a returned message blocks it and marks the editor invalid. |
| `sortable` | `boolean` | Set `false` to disable sorting for this column. |
| `filterable` | `boolean` | Set `false` to remove the column's filter button. |
| `filter` | `({ column, table, labels }) => { element, update?, dispose? }` | Custom filter UI rendered in the filter row instead of the filter button (requires `columnFilters`). |
| `filterFn` | `(value, filterValue, item) => boolean` | Custom predicate replacing the default case-insensitive contains. |
| `filterType` | `'text' \| 'number' \| 'date' \| 'select'` | Operator set for the built-in [filter menu](https://coreui.io/data-grid/docs/features/filtering/); `select` renders the faceted set filter. Defaults to `text`. |
| `resizable` | `boolean` | Set `false` to drop the resize handle for this column (when `columnSizing` is on). |
| `movable` | `boolean` | Set `false` to exclude the column from drag-and-drop reordering. |
| `hideable` | `boolean` | Set `false` to prevent hiding the column. |
| `formatter` | `(value, item) => string` | Formats the cell value — cheap, stays on the scroll hot path. |
| `render` | `(item, index) => Node \| string` | Full custom cell content (e.g. action buttons). |
| `width` | `number` | Initial column width in pixels - seeds `columnSizing` and drives the layout. |
| `style` | `object` | Inline styles for the header cell (cosmetic; e.g. percentage widths are visual-only). |
`formatter` runs on the scroll hot path and its output is used for
[CSV export](https://coreui.io/data-grid/docs/features/csv-export/); `render` is for rich cell content and is
never exported. Use one or the other per column.
---
# Data Grid Events
> CoreUI Data Grid events — namespaced *.coreui.data-grid, each carrying structured state.
All events are namespaced `*.coreui.data-grid` and carry structured state.
Listen with `element.addEventListener('sortingChange.coreui.data-grid', handler)`.
| Event | Payload |
| --- | --- |
| `sortingChange` | `{ sorting: SortingState }` |
| `filterChange` | `{ columnFilters: ColumnFiltersState, globalFilter: string }` |
| `selectionChange` | `{ rowSelection: RowSelectionState, selectedItems: object[] }` |
| `paginationChange` | `{ pagination: PaginationState }` |
| `sizingChange` | `{ columnSizing: ColumnSizingState }` |
| `pinningChange` | `{ columnPinning: ColumnPinningState }` |
| `orderChange` | `{ columnOrder: ColumnOrderState }` |
| `visibilityChange` | `{ columnVisibility: ColumnVisibilityState }` |
| `rowOrderChange` | `{ items: object[], item: object, oldIndex: number, newIndex: number }` — the grid never mutates `items`; apply the reordered array yourself. See [Row reordering](https://coreui.io/data-grid/docs/features/row-reordering/) |
| `editStart` | `{ item: object, columnId: string }` |
| `editCommit` | `{ item: object, columnId: string, value: unknown, previousValue: unknown }` — the grid never mutates `items`; apply the change yourself. [Undo/redo](https://coreui.io/data-grid/docs/features/history/) re-emits it with the values swapped |
| `editCancel` | `{ item: object, columnId: string }` |
| `cellCopy` | `{ text: string }` — fires after Ctrl/Cmd+C copies a [cell selection](https://coreui.io/data-grid/docs/features/cell-selection/), with the text written to the clipboard |
| `dataLoad` | `{ items: object[], totalRows: number }` (server-side mode) |
| `dataError` | `{ error: unknown }` (server-side mode) |
---
# Data Grid Methods
> CoreUI Data Grid instance methods — selection, CSV export, updating options, disposal and the headless table getter.
Call these on the `DataGrid` instance returned by the constructor.
| Method | Description |
| --- | --- |
| `getSelectedItems()` | Returns the selected item objects. |
| `getCsv(options?)` | Returns the rows as an RFC-4180 CSV string. Options: `scope`, `delimiter`, `bom`, `sanitize`. See [CSV export](https://coreui.io/data-grid/docs/features/csv-export/). |
| `downloadCsv(options?)` | Downloads the CSV as a file. Same options plus `filename`. |
| `print()` | Prints every sorted and filtered row through a plain generated table, past virtualization and pagination. See [Print](https://coreui.io/data-grid/docs/features/print/). |
| `getState()` | Serializable snapshot of every user-adjustable state slice. See [Save & restore state](https://coreui.io/data-grid/docs/features/state/). |
| `restoreState(state)` | Applies a (partial) snapshot back onto the grid. |
| `undo()` | Reverts the most recent [edit commit](https://coreui.io/data-grid/docs/features/history/) by re-emitting `editCommit` with the values swapped. Requires `history: true`. |
| `redo()` | Re-applies the most recently undone edit commit. |
| `setItems(items)` | Replaces the data in place, preserving sorting, filters, selection and scroll. Pair with `itemKey` so selection follows items rather than row indexes. Ignored in [server-side mode](https://coreui.io/data-grid/docs/features/server-side-data/), where the `dataProvider` owns the data. |
| `update(options)` | Merges options and performs a full rebuild — sorting, filters, selection and scroll are reset. Use `setItems()` to swap data while keeping user state. |
| `dispose()` | Destroys the instance and removes the rendered UI. |
| `table` (getter) | The underlying headless table instance — the [headless escape hatch](https://coreui.io/data-grid/docs/api/headless/) for building custom UI. |
---
# Data Grid Headless Table
> Drop down to the underlying headless table instance to build custom Data Grid UI and drive state imperatively.
The Data Grid is a thin, styled layer over a headless table engine. When the
built-in chrome isn't enough, turn it off and drive the grid from your own UI
through the `table` getter — the same instance the grid renders from.
```js
const grid = new coreui.DataGrid(element, {
columns,
items,
pagination: false, // turn off built-in chrome you want to replace
})
// Drive state imperatively through the underlying table instance:
grid.table.setPageIndex(3)
grid.table.getFilteredRowModel()
grid.table.setColumnPinning({ left: ['name'] })
grid.table.store.state.sorting
```
## When to reach for it
- **Custom chrome.** Build your own toolbar, pager or column chooser and wire it
to `grid.table.*`. The [slots](https://coreui.io/data-grid/docs/features/slots/) API hands you the same `table`
scoped to a mount point — prefer slots when you only need to replace one module.
- **Reading state.** `grid.table.store.state` exposes sorting, filters,
selection, pagination, pinning, order and visibility as structured state.
- **Imperative actions.** Set the page, toggle a column, change pinning or apply
a filter without waiting for user interaction.
## Notes
Everything the built-in UI does routes through this same table, so your
imperative calls and the built-in controls stay in sync. Grid
[events](https://coreui.io/data-grid/docs/api/events/) fire for headless-driven changes too.
---
# Data Grid Roadmap
> What CoreUI Data Grid ships today and what is planned next — tree data, master-detail panels, a summary row, grouping and aggregation, and multi-level headers.
The Data Grid is under active development. Everything documented here works
today — this page is about what comes next.
## Shipped
The 0.1.0 line built the grid's core; 0.2.0 made it interactive.
- **Data at scale** — [virtualization](https://coreui.io/data-grid/docs/features/virtualization/) for 100,000
rows with DOM recycling, [pagination](https://coreui.io/data-grid/docs/features/pagination/) as the
alternative mode, [server-side data](https://coreui.io/data-grid/docs/features/server-side-data/) behind a
single `dataProvider`, and [infinite scroll](https://coreui.io/data-grid/docs/features/infinite-scroll/)
that appends each page instead of replacing it.
- **Querying** — multi-column [sorting](https://coreui.io/data-grid/docs/features/sorting/), per-column
[filter dialogs](https://coreui.io/data-grid/docs/features/filtering/) with typed operators, faceted set
filters and custom predicates, plus global search.
- **Columns** — [resizing](https://coreui.io/data-grid/docs/columns/sizing/), [pinning](https://coreui.io/data-grid/docs/columns/pinning/),
[reordering and visibility](https://coreui.io/data-grid/docs/columns/ordering-visibility/) with a live
drag preview, and a keyboard-accessible [header menu](https://coreui.io/data-grid/docs/columns/menu/).
- **Rows** — [row selection](https://coreui.io/data-grid/docs/features/row-selection/),
[row reordering](https://coreui.io/data-grid/docs/features/row-reordering/) by drag handle, and
[auto row height](https://coreui.io/data-grid/docs/features/virtualization/#auto-row-height) for rows that
grow with their content.
- **Cells** — [keyboard navigation](https://coreui.io/data-grid/docs/features/keyboard-navigation/) on the full
ARIA grid pattern, [cell selection](https://coreui.io/data-grid/docs/features/cell-selection/) with
spreadsheet-style ranges and clipboard copy,
[inline editing](https://coreui.io/data-grid/docs/features/editing/) with a popup editor contract, and
[undo & redo](https://coreui.io/data-grid/docs/features/history/).
- **Output** — [Excel export](https://coreui.io/data-grid/docs/features/excel-export/) through a real `.xlsx`
writer with no runtime dependencies, [CSV export](https://coreui.io/data-grid/docs/features/csv-export/) as a
dependency-free subpath, and [print](https://coreui.io/data-grid/docs/features/print/) past virtualization
and pagination.
- **Fit and finish** — [save & restore state](https://coreui.io/data-grid/docs/features/state/) for the whole
view, [feature sets](https://coreui.io/data-grid/docs/customization/feature-sets/) that drop what you do not
use from the bundle, [theming](https://coreui.io/data-grid/docs/customization/styling/) through design tokens
with automatic dark mode, a complete
[localization](https://coreui.io/data-grid/docs/customization/localization/) surface, and
[accessibility](https://coreui.io/data-grid/docs/guides/accessibility/) documented down to its current limits.
## Planned
Roughly in the order we expect to reach them, with no dates attached:
- **Tree data** — hierarchical, expandable rows with tree-aware sorting and
filtering.
- **Master-detail panels** — an expandable panel per row for detail views.
- **Summary row** — totals, averages and custom aggregates in a footer under the
table.
- **Row grouping & aggregation** — group rows by column values, with aggregates
per group.
- **Column grouping** — multi-level headers spanning related columns.
- **Row pinning** — sticky rows at the top or bottom, the row-wise counterpart
of column pinning.
- **Cell & row spanning** — merged cells that keyboard navigation understands.
- **Column autosizing** — fit a column to its content from the resize handle or
the header menu.
- **Clipboard paste** — writing a copied range back through the editing
pipeline, with validation and rollback.