@ceriousdevtech/vue-cerious-scroll
v1.0.6
Published
Vue 3 bindings for CeriousScroll — high-performance virtual scrolling with O(1) memory and no height estimation
Maintainers
Readme
@ceriousdevtech/vue-cerious-scroll
Vue 3 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 Vue's synchronous render(), so every row's real height is measured (never estimated) — exactly the guarantee that makes CeriousScroll precise. Rows are rendered with your app's appContext, so globally registered components, directives, and installed plugins work normally inside each row.
Installation
npm install @ceriousdevtech/vue-cerious-scroll @ceriousdevtech/cerious-scrollvue (>= 3.3) is a peer dependency.
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 demo # dev server with HMR
npm run demo:build # production build to demo/distThe demo imports the wrapper by its package name, aliased to the library source,
so edits to src/ are reflected live.
Quick start (component)
Give the container a height; provide items and an #item scoped slot.
<script setup lang="ts">
import { CeriousScroll } from '@ceriousdevtech/vue-cerious-scroll';
const items = Array.from({ length: 1_000_000 }, (_, i) => ({ id: i, name: `Item ${i}` }));
</script>
<template>
<CeriousScroll :items="items" :style="{ height: '480px' }">
<template #item="{ item, index }">
<div class="row">{{ index }} — {{ item.name }}</div>
</template>
</CeriousScroll>
</template>Variable heights need no configuration — just render rows of whatever height; the engine measures each one.
Without a full array (huge / sparse data)
<CeriousScroll
:total-elements="100_000_000"
:get-item="(index) => loadRow(index)"
:style="{ height: '600px' }"
>
<template #item="{ item, index }">
<Row :data="item" :index="index" />
</template>
</CeriousScroll>Composable
useCeriousScroll gives you full control. Attach containerRef to your scroll
element; the composable renders the rows imperatively into the engine's measured
containers.
<script setup lang="ts">
import { h } from 'vue';
import { useCeriousScroll } from '@ceriousdevtech/vue-cerious-scroll';
const { containerRef } = useCeriousScroll({
items,
renderItem: (item, index) => h('div', { class: 'row' }, `${index} — ${item.name}`),
});
</script>
<template>
<div ref="containerRef" style="height: 480px; position: relative; overflow: hidden" />
</template>
renderItemreturns a VueVNodeChild(useh(...), or render JSX/TSX).
Component props
| Prop | 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. |
| renderItem | (item, index) => VNodeChild | Render prop alternative to the #item scoped slot. |
| options | CeriousScrollOptions | Engine options (keyboard/touch/wheel/scrollbar/etc.). Read once at creation. |
| autoRender | boolean | Re-render on scroll/resize/data changes. Default true. |
The row is provided by the #item scoped slot ({ item, index }) or the
render-item prop. In table mode, a #header slot renders the <thead> row
(see Table layout). Apply class / style directly to the
component — they fall through onto the scroll container (set a height!).
Events
| Event | Payload | Description |
| --- | --- | --- |
| viewport-change | CeriousViewportChangeDetail | Normalized viewport-change (wheel/touch/keyboard/scrollbar). |
| measured-viewport | MeasuredViewportRange | Measured range after each render pass. |
| ready | CeriousScrollEngine | The underlying engine instance, once ready. |
Imperative API (via template ref)
const scroll = ref<InstanceType<typeof CeriousScroll> | null>(null);
// scroll.value?.jumpToElement(500);
// scroll.value?.scrollToPercentage(50);
// scroll.value?.reset();
// scroll.value?.render();
// scroll.value?.recalculate(); // drop cached heights + re-measure (see Notes)
// scroll.value?.scroller; // the raw engineTable layout
Pass :options="{ layout: 'table' }" to render real <table> / <tr> / <td> rows with a frozen header and native column alignment. The #item slot returns the row's <td> cells; a #header slot provides the (declarative, reactive) <thead> row:
<CeriousScroll
class="my-scroll"
:total-elements="100000"
:get-item="(i) => i"
:options="{ layout: 'table', table: { tableClassName: 'my-table', autoSizeColumns: true } }"
>
<template #header>
<tr><th v-for="c in columns" :key="c.key">{{ c.label }}</th></tr>
</template>
<template #item="{ item: index }">
<td>{{ row(index).id }}</td>
<td>{{ row(index).name }}</td>
<td>{{ row(index).email }}</td>
</template>
</CeriousScroll>- The
#headerslot renders into the engine's<thead>(same<table>as the rows → native column alignment, frozen header) and stays reactive. - The
#itemslot must return<td>s. They render into the row's<tr>via adisplay: contentswrapper that isolates Vue's renderer from the engine's row recycling. 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 with Vue's synchronous
render()so the engine measures realoffsetHeight. Later size changes are picked up by the engine's built-inResizeObserver. optionsare read at creation. Changingoptionsafter mount has no effect; remount (e.g. with a:key) 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; Vue 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 templateref, or from the composable result) 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-MIT).
