# Angular Data Grid Infinite Scroll

> Load a server-side dataset page by page as the user scrolls — the Angular 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/angular/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/angular/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.

```ts
import { Component } from '@angular/core'
import { DataGridComponent } from '@coreui/angular-data-grid'
import type { DataGridColumn, DataGridDataRequest, DataGridItem } from '@coreui/angular-data-grid'

@Component({
  selector: 'docs-data-grid-infinite-example',
  imports: [DataGridComponent],
  template: `
    <!-- The token is inherited, so it reaches the grid from any ancestor. -->
    <div style="--cui-data-grid-viewport-max-height: 400px">
      <c-data-grid
        [columns]="columns"
        [itemKey]="itemKey"
        [infiniteScroll]="{ pageSize: 50 }"
        [dataProvider]="dataProvider"
      />
    </div>
  `
})
export class DataGridInfiniteExample {
  readonly columns: DataGridColumn[] = [
    { 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' }
  ]
readonly itemKey = (item: DataGridItem) => String(item.id)
readonly dataProvider = async ({ sorting, pagination }: DataGridDataRequest) => {
    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

```html
<c-data-grid
  [columns]="columns"
  [dataProvider]="dataProvider"
  [infiniteScroll]="{
    pageSize: 50,
    threshold: 10
  }"
/>
```

`pageSize` is the rows per request (default `50`); `threshold` is the rows left
below the last rendered one before the next page is requested (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/angular/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) with { items, totalRows }` with that page's items.
  `(paginationChange)` 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/angular/docs/features/server-side-data/#selection-semantics): `getSelectedItems()` and
every [CSV](https://coreui.io/data-grid/angular/docs/features/csv-export/) or [Excel](https://coreui.io/data-grid/angular/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.
