Documentation
Infinite Scroll
Load more rows as the user scrolls with onLoadMore.
Load more on scroll
Give the table a height so its body scrolls, then append rows in onLoadMore. Pair with isLoading so skeleton rows append under existing data.
React TSX
Copy
const [rows, setRows] = useState(initialRows);const [isLoading, setIsLoading] = useState(false);const handleLoadMore = async () => {if (isLoading) return;setIsLoading(true);setRows((prev) => [...prev, ...(await fetchMore(prev.length))]);setIsLoading(false);};<SimpleTablecolumns={columns}rows={rows}height="400px"onLoadMore={handleLoadMore}isLoading={isLoading}/>
Angular
Copy
rows = initialRows;isLoading = false;handleLoadMore = async () => {if (this.isLoading) return;this.isLoading = true;this.rows = [...this.rows, ...(await fetchMore(this.rows.length))];this.isLoading = false;};<simple-table[columns]="columns"[rows]="rows"height="400px"(loadMore)="handleLoadMore()"[isLoading]="isLoading"></simple-table>
Vue SFC
Copy
<script setup>import { ref } from "vue";const rows = ref(initialRows);const isLoading = ref(false);const handleLoadMore = async () => {if (isLoading.value) return;isLoading.value = true;rows.value = [...rows.value, ...(await fetchMore(rows.value.length))];isLoading.value = false;};</script><template><SimpleTable:columns="columns":rows="rows"height="400px":on-load-more="handleLoadMore":is-loading="isLoading"/></template>
Svelte
Copy
<script>let rows = $state(initialRows);let isLoading = $state(false);const handleLoadMore = async () => {if (isLoading) return;isLoading = true;rows = [...rows, ...(await fetchMore(rows.length))];isLoading = false;};</script><SimpleTable{columns}{rows}height="400px"onLoadMore={handleLoadMore}isLoading={isLoading}/>
Solid TSX
Copy
const [rows, setRows] = createSignal(initialRows);const [isLoading, setIsLoading] = createSignal(false);const handleLoadMore = async () => {if (isLoading()) return;setIsLoading(true);setRows([...rows(), ...(await fetchMore(rows().length))]);setIsLoading(false);};<SimpleTablecolumns={columns}rows={rows()}height="400px"onLoadMore={handleLoadMore}isLoading={isLoading()}/>
TypeScript
Copy
let rows = initialRows;const table = new SimpleTableVanilla(container, {columns,rows,height: "400px",isLoading: false,onLoadMore: async () => {table.update({ isLoading: true });rows = [...rows, ...(await fetchMore(rows.length))];table.update({ rows, isLoading: false });},});
Page / external scroll
Omit height / maxHeight and set scrollParent="window" (or a container / getter). The parent scroll drives virtualization and onLoadMore; the header sticks to the top of that viewport.
React TSX
Copy
<SimpleTablecolumns={columns}rows={rows}scrollParent="window"onLoadMore={handleLoadMore}isLoading={isLoading}/>
Angular
Copy
<simple-table[columns]="columns"[rows]="rows"scrollParent="window"(loadMore)="handleLoadMore()"[isLoading]="isLoading"></simple-table>
Vue SFC
Copy
<SimpleTable:columns="columns":rows="rows"scroll-parent="window":on-load-more="handleLoadMore":is-loading="isLoading"/>
Svelte
Copy
<SimpleTable{columns}{rows}scrollParent="window"onLoadMore={handleLoadMore}isLoading={isLoading}/>
Solid TSX
Copy
<SimpleTablecolumns={columns}rows={rows()}scrollParent="window"onLoadMore={handleLoadMore}isLoading={isLoading()}/>
TypeScript
Copy
new SimpleTableVanilla(container, {columns,rows,scrollParent: "window",onLoadMore: handleLoadMore,});
height vs scrollParent
If height or maxHeight is set, scrollParent is ignored. Without either, all rows render and onLoadMore does not fire.
Example
Scroll to the bottom to load more batches. Use Code or StackBlitz for the full example.
20 rows loaded
React TSX
Copy
1import { useState, useCallback } from "react";2import { SimpleTable } from "@simple-table/react";3import type { Theme } from "@simple-table/react";4import {5 infiniteScrollConfig,6 generateInfiniteScrollData,7 type InfiniteScrollEmployee8} from "./infinite-scroll.demo-data";9import "@simple-table/react/styles.css";1011const MAX_ROWS = 200;12const BATCH_SIZE = 15;1314const InfiniteScrollDemo = ({15 height = "400px",16 theme17}: {18 height?: string | number;19 theme?: Theme;20}) => {21 const [rows, setRows] = useState<InfiniteScrollEmployee[]>(() =>22 generateInfiniteScrollData(0, 30),23 );24 const [loading, setLoading] = useState(false);25 const [hasMore, setHasMore] = useState(true);2627 const handleLoadMore = useCallback(() => {28 if (loading || !hasMore) return;29 setLoading(true);3031 setTimeout(() => {32 setRows((prev) => {33 const newRows = generateInfiniteScrollData(prev.length, BATCH_SIZE);34 const updated = [...prev, ...newRows];35 if (updated.length >= MAX_ROWS) {36 setHasMore(false);37 }38 return updated;39 });40 setLoading(false);41 }, 500);42 }, [loading, hasMore]);4344 return (45 <div>46 <div style={{ marginBottom: 8, fontSize: 13, color: "#666" }}>47 {rows.length} rows loaded{hasMore ? "" : " (all loaded)"}48 </div>49 <SimpleTable50 columns={infiniteScrollConfig.headers}51 getRowId={({ row }) => row.id}52 rows={rows}53 onLoadMore={handleLoadMore}54 isLoading={loading}55 height={height}56 theme={theme}57 />58 </div>59 );60};6162export default InfiniteScrollDemo;
Angularinfinite-scroll-demo.component.ts
Copy
1import { Component, Input } from "@angular/core";2import { SimpleTableImports } from "@simple-table/angular";3import type { AngularColumnDef, GetRowIdParams, Theme } from "@simple-table/angular";4import { infiniteScrollConfig, generateInfiniteScrollData } from "./infinite-scroll.demo-data";5import "@simple-table/angular/styles.css";6import type { InfiniteScrollEmployee } from "./infinite-scroll.demo-data";78const MAX_ROWS = 200;9const BATCH_SIZE = 15;1011@Component({12 selector: "infinite-scroll-demo",13 standalone: true,14 imports: [SimpleTableImports],15 template: `16 <div>17 <div style="margin-bottom: 8px; font-size: 13px; color: #666">18 {{ rows.length }} rows loaded{{ hasMore ? '' : ' (all loaded)' }}19 </div>20 <simple-table21 [getRowId]="getRowId"22 [rows]="rows"23 [columns]="headers"24 [isLoading]="loading"25 [height]="height"26 [theme]="theme"27 (loadMore)="handleLoadMore()"28 ></simple-table>29 </div>30 `,31})32export class InfiniteScrollDemoComponent {33 @Input() height: string | number = "400px";34 @Input() theme?: Theme;3536 readonly headers: AngularColumnDef<InfiniteScrollEmployee>[] = infiniteScrollConfig.headers;37 rows: InfiniteScrollEmployee[] = generateInfiniteScrollData(0, 30) ;38 loading = false;39 hasMore = true;4041 handleLoadMore = () => {42 if (this.loading || !this.hasMore) return;43 this.loading = true;44 setTimeout(() => {45 const newRows = generateInfiniteScrollData(this.rows.length, BATCH_SIZE) ;46 this.rows = [...this.rows, ...newRows];47 if (this.rows.length >= MAX_ROWS) this.hasMore = false;48 this.loading = false;49 }, 500);50 };5152 getRowId = ({ row }: GetRowIdParams<InfiniteScrollEmployee>) => row.id;53}545556// infinite-scroll.demo-data.ts57// Self-contained demo table setup for this example.58import type { AngularColumnDef, ValueFormatterProps } from "@simple-table/angular";5960export interface InfiniteScrollEmployee {61 id: number;62 name: string;63 email: string;64 department: string;65 salary: number;66}6768const FIRST_NAMES = ["Elena", "Kai", "Amara", "Santiago", "Priya", "Magnus", "Zara", "Luca", "Sarah", "Olumide", "Isabella", "Dmitri"];69const LAST_NAMES = ["Vasquez", "Tanaka", "Okafor", "Rodriguez", "Chakraborty", "Eriksson", "Al-Rashid", "Rossi", "Kim", "Adebayo", "Chen", "Volkov"];70const DEPARTMENTS = ["Engineering", "AI Research", "UX Design", "DevOps", "Marketing", "Product", "Sales", "Finance"];7172export function generateInfiniteScrollData(startIndex: number, count: number): InfiniteScrollEmployee[] {73 const rows: InfiniteScrollEmployee[] = [];74 for (let i = 0; i < count; i++) {75 const idx = startIndex + i;76 const first = FIRST_NAMES[idx % FIRST_NAMES.length];77 const last = LAST_NAMES[idx % LAST_NAMES.length];78 rows.push({79 id: idx + 1,80 name: `${first} ${last}`,81 email: `${first.toLowerCase()}.${last.toLowerCase()}@techcorp.com`,82 department: DEPARTMENTS[idx % DEPARTMENTS.length],83 salary: 60000 + Math.floor(((idx * 7919) % 100000)),84 });85 }86 return rows;87}8889export const infiniteScrollHeaders: AngularColumnDef<InfiniteScrollEmployee, any>[] = [90 { accessor: "id", label: "ID", width: 80, type: "number" },91 { accessor: "name", label: "Name", width: "1fr", minWidth: 120 },92 { accessor: "email", label: "Email", width: 250 },93 { accessor: "department", label: "Department", width: 150 },94 {95 accessor: "salary",96 label: "Salary",97 width: 120,98 type: "number",99 align: "right",100 valueFormatter: ({ value }: ValueFormatterProps<InfiniteScrollEmployee, number>) => `$${value.toLocaleString()}`,101 },102];103104export const infiniteScrollConfig = {105 headers: infiniteScrollHeaders,106 rows: generateInfiniteScrollData(0, 30),107};108
Vue SFC
Copy
1<script setup lang="ts">2import { ref, computed } from "vue";3import { SimpleTable } from "@simple-table/vue";4import type { Theme, GetRowIdParams } from "@simple-table/vue";5import { infiniteScrollConfig, generateInfiniteScrollData } from "./infinite-scroll.demo-data";6import type { InfiniteScrollEmployee } from "./infinite-scroll.demo-data";7import "@simple-table/vue/styles.css";89const props = withDefaults(defineProps<{ height?: string | number; theme?: Theme }>(), {10 height: "400px",11});1213const MAX_ROWS = 200;14const BATCH_SIZE = 15;1516const rows = ref<InfiniteScrollEmployee[]>(generateInfiniteScrollData(0, 30));17const loading = ref(false);18const hasMore = ref(true);1920const getRowId = ({ row }: GetRowIdParams<InfiniteScrollEmployee>) => row.id;2122const statusText = computed(() =>23 `${rows.value.length} rows loaded${hasMore.value ? "" : " (all loaded)"}`,24);2526function handleLoadMore() {27 if (loading.value || !hasMore.value) return;28 loading.value = true;29 setTimeout(() => {30 const newRows = generateInfiniteScrollData(rows.value.length, BATCH_SIZE);31 rows.value = [...rows.value, ...newRows];32 if (rows.value.length >= MAX_ROWS) hasMore.value = false;33 loading.value = false;34 }, 500);35}36</script>3738<template>39 <div>40 <div style="margin-bottom: 8px; font-size: 13px; color: #666">{{ statusText }}</div>41 <SimpleTable42 :columns="infiniteScrollConfig.headers"43 :get-row-id="getRowId"44 :rows="rows"45 :is-loading="loading"46 :on-load-more="handleLoadMore"47 :height="props.height"48 :theme="props.theme"49 />50 </div>51</template>
Svelte
Copy
1<script lang="ts">2 import { SimpleTable } from "@simple-table/svelte";3 import type { Theme, GetRowIdParams } from "@simple-table/svelte";4 import { infiniteScrollConfig, generateInfiniteScrollData } from "./infinite-scroll.demo-data";5 import type { InfiniteScrollEmployee } from "./infinite-scroll.demo-data";6 import "@simple-table/svelte/styles.css";78 let { height = "400px", theme }: { height?: string | number; theme?: Theme } = $props();910 const MAX_ROWS = 200;11 const BATCH_SIZE = 15;1213 let rows = $state<InfiniteScrollEmployee[]>(generateInfiniteScrollData(0, 30));14 let loading = $state(false);15 let hasMore = $state(true);1617 const getRowId = ({ row }: GetRowIdParams<InfiniteScrollEmployee>) => row.id;1819 function handleLoadMore() {20 if (loading || !hasMore) return;21 loading = true;22 setTimeout(() => {23 const newRows = generateInfiniteScrollData(rows.length, BATCH_SIZE);24 rows = [...rows, ...newRows];25 if (rows.length >= MAX_ROWS) hasMore = false;26 loading = false;27 }, 500);28 }29</script>3031<div>32 <div style="margin-bottom: 8px; font-size: 13px; color: #666">33 {rows.length} rows loaded{hasMore ? "" : " (all loaded)"}34 </div>35 <SimpleTable36 columns={infiniteScrollConfig.headers}37 {rows}38 {getRowId}39 isLoading={loading}40 onLoadMore={handleLoadMore}41 {height}42 {theme}43 />44</div>
Solid TSX
Copy
1import { createSignal, createMemo } from "solid-js";2import { SimpleTable } from "@simple-table/solid";3import type { Theme } from "@simple-table/solid";4import {5 infiniteScrollConfig,6 generateInfiniteScrollData,7 type InfiniteScrollEmployee,8} from "./infinite-scroll.demo-data";9import "@simple-table/solid/styles.css";1011const MAX_ROWS = 200;12const BATCH_SIZE = 15;1314export default function InfiniteScrollDemo(props: { height?: string | number; theme?: Theme }) {15 const [rows, setRows] = createSignal<InfiniteScrollEmployee[]>(generateInfiniteScrollData(0, 30));16 const [loading, setLoading] = createSignal(false);17 const [hasMore, setHasMore] = createSignal(true);1819 const statusText = createMemo(() =>20 `${rows().length} rows loaded${hasMore() ? "" : " (all loaded)"}`,21 );2223 const handleLoadMore = () => {24 if (loading() || !hasMore()) return;25 setLoading(true);26 setTimeout(() => {27 setRows((prev) => {28 const newRows = generateInfiniteScrollData(prev.length, BATCH_SIZE);29 const updated = [...prev, ...newRows];30 if (updated.length >= MAX_ROWS) setHasMore(false);31 return updated;32 });33 setLoading(false);34 }, 500);35 };3637 return (38 <div>39 <div style={{ "margin-bottom": "8px", "font-size": "13px", color: "#666" }}>40 {statusText()}41 </div>42 <SimpleTable43 columns={infiniteScrollConfig.headers}44 getRowId={({ row }) => row.id}45 rows={rows()}46 onLoadMore={handleLoadMore}47 isLoading={loading()}48 height={props.height ?? "400px"}49 theme={props.theme}50 />51 </div>52 );53}
TypeScriptInfiniteScrollDemo.ts
Copy
1import { SimpleTableVanilla } from "simple-table-core";2import type { InfiniteScrollEmployee } from "./infinite-scroll.demo-data";3import type { Theme, GetRowIdParams } from "simple-table-core";4import { infiniteScrollConfig, generateInfiniteScrollData } from "./infinite-scroll.demo-data";5import "simple-table-core/styles.css";67const MAX_ROWS = 200;8const BATCH_SIZE = 15;91011const getRowId = ({ row }: GetRowIdParams<InfiniteScrollEmployee>) => row.id;12export function renderInfiniteScrollDemo(13 container: HTMLElement,14 options?: { height?: string | number; theme?: Theme }15): SimpleTableVanilla<InfiniteScrollEmployee> {16 const wrapper = document.createElement("div");1718 const status = document.createElement("div");19 Object.assign(status.style, { marginBottom: "8px", fontSize: "13px", color: "#666" });2021 wrapper.appendChild(status);2223 const tableContainer = document.createElement("div");24 wrapper.appendChild(tableContainer);25 container.appendChild(wrapper);2627 let rows: InfiniteScrollEmployee[] = generateInfiniteScrollData(0, 30);28 let loading = false;29 let hasMore = true;3031 const updateStatus = () => {32 status.textContent = `${rows.length} rows loaded${hasMore ? "" : " (all loaded)"}`;33 };34 updateStatus();3536 const table = new SimpleTableVanilla(tableContainer, {37 getRowId,38 columns: infiniteScrollConfig.headers,39 rows,40 height: options?.height ?? "400px",41 theme: options?.theme,42 onLoadMore: () => {43 if (loading || !hasMore) return;44 loading = true;45 table.update({ isLoading: true });46 setTimeout(() => {47 const newRows = generateInfiniteScrollData(rows.length, BATCH_SIZE) ;48 rows = [...rows, ...newRows];49 if (rows.length >= MAX_ROWS) hasMore = false;50 loading = false;51 table.update({ rows, isLoading: false });52 updateStatus();53 }, 500);54 },55 });5657 return table;58}596061// infinite-scroll.demo-data.ts62// Self-contained demo table setup for this example.63import type { ColumnDef } from "simple-table-core";6465export interface InfiniteScrollEmployee {66 id: number;67 name: string;68 email: string;69 department: string;70 salary: number;71}7273const FIRST_NAMES = ["Elena", "Kai", "Amara", "Santiago", "Priya", "Magnus", "Zara", "Luca", "Sarah", "Olumide", "Isabella", "Dmitri"];74const LAST_NAMES = ["Vasquez", "Tanaka", "Okafor", "Rodriguez", "Chakraborty", "Eriksson", "Al-Rashid", "Rossi", "Kim", "Adebayo", "Chen", "Volkov"];75const DEPARTMENTS = ["Engineering", "AI Research", "UX Design", "DevOps", "Marketing", "Product", "Sales", "Finance"];7677export function generateInfiniteScrollData(startIndex: number, count: number): InfiniteScrollEmployee[] {78 const rows: InfiniteScrollEmployee[] = [];79 for (let i = 0; i < count; i++) {80 const idx = startIndex + i;81 const first = FIRST_NAMES[idx % FIRST_NAMES.length];82 const last = LAST_NAMES[idx % LAST_NAMES.length];83 rows.push({84 id: idx + 1,85 name: `${first} ${last}`,86 email: `${first.toLowerCase()}.${last.toLowerCase()}@techcorp.com`,87 department: DEPARTMENTS[idx % DEPARTMENTS.length],88 salary: 60000 + Math.floor(((idx * 7919) % 100000)),89 });90 }91 return rows;92}9394export const infiniteScrollHeaders: ColumnDef<InfiniteScrollEmployee>[] = [95 { accessor: "id", label: "ID", width: 80, type: "number" },96 { accessor: "name", label: "Name", width: "1fr", minWidth: 120 },97 { accessor: "email", label: "Email", width: 250 },98 { accessor: "department", label: "Department", width: 150 },99 {100 accessor: "salary",101 label: "Salary",102 width: 120,103 type: "number",104 align: "right",105 valueFormatter: ({ value }) => `$${Number(value).toLocaleString()}`,106 },107];108109export const infiniteScrollConfig = {110 headers: infiniteScrollHeaders,111 rows: generateInfiniteScrollData(0, 30),112};113
Props
Infinite Scroll Configuration
| Property | Required | Description | Example |
|---|---|---|---|
Property | Required | Description | Example |
onLoadMore() => void | Optional | Fires when the user scrolls near the bottom. Append the next batch of rows. | |
infiniteScrollThresholdnumber | Optional | Pixels from the bottom at which onLoadMore fires. Defaults to 200. | |
heightstring | number | Optional | Fixed table height for inner scrolling. Use this or scrollParent. | |
scrollParentHTMLElement | "window" | (() => HTMLElement | null) | Optional | External scroll container when height/maxHeight are unset. Accepts an element, "window", or a getter. | |
isLoadingboolean | Optional | Appends skeleton rows under existing data while the next page loads. |