@ceriousdevtech/ngx-cerious-scroll
June 8, 2026 · View on GitHub
Angular bindings for Cerious Scroll™ — high-performance virtual scrolling with O(1) memory, consistent 60 FPS+, and native variable-height support with no height estimation.
Rows are rendered into the engine's own measured containers via Angular's EmbeddedViewRef and committed synchronously, so every row's real height is measured (never estimated) — exactly the guarantee that makes CeriousScroll precise. Because rows stay in your Angular tree, DI, pipes, and structural directives work normally inside each row.
Installation
npm install @ceriousdevtech/ngx-cerious-scroll @ceriousdevtech/cerious-scroll
@angular/core and @angular/common (>= 16) are peer dependencies.
Demo
Live demo → — 100,000 rows, fixed/variable-height toggle, imperative jump-to-row, and live viewport stats.
To run locally:
npm install
npm run build # build the library
npm start # dev server with HMR (http://localhost:4300/)
The demo imports the wrapper by its package name, aliased to the built library,
so rebuild the library after editing projects/ngx-cerious-scroll/src/.
Quick start (component)
Give the host a height; provide items and an <ng-template ceriousScrollItem>.
import { Component } from '@angular/core';
import {
CeriousScrollComponent,
CeriousScrollItemTemplateDirective,
} from '@ceriousdevtech/ngx-cerious-scroll';
@Component({
standalone: true,
imports: [CeriousScrollComponent, CeriousScrollItemTemplateDirective],
template: `
<cerious-scroll [items]="items" style="height: 480px">
<ng-template ceriousScrollItem let-item let-index="index">
<div class="row">{{ index }} — {{ item.name }}</div>
</ng-template>
</cerious-scroll>
`,
})
export class List {
items = Array.from({ length: 1_000_000 }, (_, i) => ({ id: i, name: `Item ${i}` }));
}
Variable heights need no configuration — just render rows of whatever height; the engine measures each one.
Without a full array (huge / sparse data)
<cerious-scroll
[totalElements]="100_000_000"
[getItem]="loadRow"
style="height: 600px"
>
<ng-template ceriousScrollItem let-item let-index="index">
<app-row [data]="item" [index]="index" />
</ng-template>
</cerious-scroll>
Directive
[ceriousScroll] gives you full control on any host element. Pass a
TemplateRef via [ceriousScrollItemTemplate]; the directive renders rows
imperatively into the engine's measured containers.
import { Component, TemplateRef, ViewChild } from '@angular/core';
import { CeriousScrollDirective } from '@ceriousdevtech/ngx-cerious-scroll';
@Component({
standalone: true,
imports: [CeriousScrollDirective],
template: `
<ng-template #row let-item let-index="index">
<div class="row">{{ index }} — {{ item.name }}</div>
</ng-template>
<div
ceriousScroll
[ceriousScrollItems]="items"
[ceriousScrollItemTemplate]="row"
style="height: 480px; position: relative; overflow: hidden"
></div>
`,
})
export class List {
@ViewChild('row', { static: true }) row!: TemplateRef<any>;
items = /* ... */;
}
Component inputs
| Input | Type | Description |
|---|---|---|
items | readonly TItem[] | Optional data array. totalElements defaults to items.length. |
totalElements | number | Total item count. Required if items is omitted. |
getItem | (index) => TItem | Lazy item getter for large/sparse datasets. |
itemTemplate | TemplateRef<{ $implicit, index }> | Row template. Alternative to projecting <ng-template ceriousScrollItem>. |
headerTemplate | TemplateRef | Table mode only. <tr> of <th>s rendered into the engine's <thead> (see Table layout). |
options | CeriousScrollOptions | Engine options (keyboard/touch/wheel/scrollbar/layout/etc.). Read once at creation. |
autoRender | boolean | Re-render on scroll/resize/data changes. Default true. |
The row is provided by the projected <ng-template ceriousScrollItem let-item let-index="index"> or the itemTemplate input. Apply class / style directly to <cerious-scroll> — it's a block-level host (set a height!).
Outputs
| Output | Payload | Description |
|---|---|---|
viewportChange | CeriousViewportChangeDetail | Normalized viewport-change (wheel/touch/keyboard/scrollbar). |
measuredViewport | MeasuredViewportRange | Measured range after each render pass. |
scrollerReady | CeriousScroll | The underlying engine instance, once ready. |
Imperative API (via template reference)
<cerious-scroll #scroll [items]="items">…</cerious-scroll>
@ViewChild(CeriousScrollDirective) scroll!: CeriousScrollDirective;
// scroll.jumpToElement(500);
// scroll.scrollToPercentage(50);
// scroll.reset();
// scroll.render();
// scroll.recalculate(); // drop cached heights + re-measure (see Notes)
// scroll.hostRef?.scroller; // the raw engine
Table layout
Pass [ceriousScrollOptions]="{ layout: 'table' }" to render real <table> / <tr> / <td> rows with a frozen header and native column alignment. The row template returns the row's <td> cells; [ceriousScrollHeaderTemplate] provides the <thead> row (it updates via change detection):
<div
class="my-scroll"
ceriousScroll
[ceriousScrollTotalElements]="100000"
[ceriousScrollGetItem]="getItem"
[ceriousScrollItemTemplate]="rowTpl"
[ceriousScrollHeaderTemplate]="headerTpl"
[ceriousScrollOptions]="{ layout: 'table', table: { tableClassName: 'my-table', autoSizeColumns: true } }"
></div>
<ng-template #headerTpl>
<tr>
@for (c of columns; track c.key) { <th>{{ c.label }}</th> }
</tr>
</ng-template>
<!-- Row template roots must be <td>s (no structural directive at the root). -->
<ng-template #rowTpl let-index>
<td>{{ row(index).id }}</td>
<td>{{ row(index).name }}</td>
<td>{{ row(index).email }}</td>
</ng-template>
<cerious-scroll> exposes the same via [headerTemplate] and <ng-template ceriousScrollItem>.
- The header template renders into the engine's
<thead>(same<table>as the rows → native column alignment, frozen header). - The row template's root nodes must be
<td>s (don't wrap them in a structural directive at the root — that hides the cells from the directive's recycle re-append). table.autoSizeColumnsmeasures column widths once and pins them (auto-sized + stable); or usetable.columnWidths. Variable row heights work as usual.- CSS:
border-collapse: separateand an opaque<thead>background (see the core README's Table Layout notes).
Notes
- No height estimation. Rows are committed synchronously via
EmbeddedViewRef.detectChanges()so the engine measures realoffsetHeight. Later size changes are picked up by the engine's built-inResizeObserver. optionsare read at creation. Changingoptionsafter init has no effect; recreate the host (e.g. with*ngIftoggling) to apply new engine options.- Changing the item count recreates the engine internally (scroll position
is preserved). Mutating items without changing the count just re-renders the
content in place (cheap; Angular patches each row, so focus/selection survive)
— it does not discard cached heights, so editable grids that produce a new
itemsarray on every edit don't trigger a full viewport re-measure. - If every rendered row's height changes at once (e.g. a density/layout
switch) the cached heights become stale and rows can misalign until the next
scroll. Call
recalculate()on the directive instance right after the change to drop the height cache and re-measure. Don't call it on routine edits — a single cell edit keeps its row's size, and the engine's built-inResizeObserverpicks up any incidental resize on its own.
License
Licensed by Cerious DevTech LLC under the MIT License (see LICENSE).