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}/>
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>
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"[onLoadMore]="handleLoadMore"[isLoading]="isLoading"></simple-table>
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}/>
Vue SFC
Copy
<SimpleTable:columns="columns":rows="rows"scroll-parent="window":on-load-more="handleLoadMore":is-loading="isLoading"/>
Angular
Copy
<simple-table[columns]="columns"[rows]="rows"scrollParent="window"[onLoadMore]="handleLoadMore"[isLoading]="isLoading"></simple-table>
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";import type { Theme, Row } from "@simple-table/react";3import {4 infiniteScrollConfig,5 generateInfiniteScrollData,6} from "./infinite-scroll.demo-data";7import "@simple-table/react/styles.css";89const MAX_ROWS = 200;10const BATCH_SIZE = 15;1112const InfiniteScrollDemo = ({13 height = "400px",14 theme,15}: {16 height?: string | number;17 theme?: Theme;18}) => {19 const [rows, setRows] = useState<Row[]>(20 () => generateInfiniteScrollData(0, 30) as Row[]21 );22 const [loading, setLoading] = useState(false);23 const [hasMore, setHasMore] = useState(true);2425 const handleLoadMore = useCallback(() => {26 if (loading || !hasMore) return;27 setLoading(true);2829 setTimeout(() => {30 setRows((prev) => {31 const newRows = generateInfiniteScrollData(prev.length, BATCH_SIZE) as Row[];32 const updated = [...prev, ...newRows];33 if (updated.length >= MAX_ROWS) {34 setHasMore(false);35 }36 return updated;37 });38 setLoading(false);39 }, 500);40 }, [loading, hasMore]);4142 return (43 <div>44 <div style={{ marginBottom: 8, fontSize: 13, color: "#666" }}>45 {rows.length} rows loaded{hasMore ? "" : " (all loaded)"}46 </div>47 <SimpleTable48 columns={infiniteScrollConfig.headers}49 rows={rows}50 onLoadMore={handleLoadMore}51 isLoading={loading}52 height={height}53 theme={theme}54 />55 </div>56 );57};5859export default InfiniteScrollDemo;
Vue SFC
Copy
1<script setup lang="ts">2import { ref, computed } from "vue";3import {SimpleTable} from "@simple-table/vue";import type { Theme, Row } from "@simple-table/vue";4import { infiniteScrollConfig, generateInfiniteScrollData } from "./infinite-scroll.demo-data";5import "@simple-table/vue/styles.css";67const props = withDefaults(defineProps<{ height?: string | number; theme?: Theme }>(), {8 height: "400px",9});1011const MAX_ROWS = 200;12const BATCH_SIZE = 15;1314const rows = ref<Row[]>(generateInfiniteScrollData(0, 30) as Row[]);15const loading = ref(false);16const hasMore = ref(true);1718const statusText = computed(() =>19 `${rows.value.length} rows loaded${hasMore.value ? "" : " (all loaded)"}`20);2122function handleLoadMore() {23 if (loading.value || !hasMore.value) return;24 loading.value = true;25 setTimeout(() => {26 const newRows = generateInfiniteScrollData(rows.value.length, BATCH_SIZE) as Row[];27 rows.value = [...rows.value, ...newRows];28 if (rows.value.length >= MAX_ROWS) hasMore.value = false;29 loading.value = false;30 }, 500);31}32</script>3334<template>35 <div>36 <div style="margin-bottom: 8px; font-size: 13px; color: #666">{{ statusText }}</div>37 <SimpleTable38 :columns="infiniteScrollConfig.headers"39 :rows="rows"40 :is-loading="loading"41 :on-load-more="handleLoadMore"42 :height="props.height"43 :theme="props.theme"44 />45 </div>46</template>
Angularinfinite-scroll-demo.component.ts
Copy
1import { Component, Input } from "@angular/core";2import {SimpleTableComponent} from "@simple-table/angular";import type { AngularColumnDef, Row, Theme } from "@simple-table/angular";3import { infiniteScrollConfig, generateInfiniteScrollData } from "./infinite-scroll.demo-data";4import "@simple-table/angular/styles.css";56const MAX_ROWS = 200;7const BATCH_SIZE = 15;89@Component({10 selector: "infinite-scroll-demo",11 standalone: true,12 imports: [SimpleTableComponent],13 template: `14 <div>15 <div style="margin-bottom: 8px; font-size: 13px; color: #666">16 {{ rows.length }} rows loaded{{ hasMore ? '' : ' (all loaded)' }}17 </div>18 <simple-table19 [rows]="rows"20 [columns]="headers"21 [isLoading]="loading"22 [height]="height"23 [theme]="theme"24 [onLoadMore]="handleLoadMore"25 ></simple-table>26 </div>27 `,28})29export class InfiniteScrollDemoComponent {30 @Input() height: string | number = "400px";31 @Input() theme?: Theme;3233 readonly headers: AngularColumnDef[] = infiniteScrollConfig.headers;34 rows: Row[] = generateInfiniteScrollData(0, 30) as Row[];35 loading = false;36 hasMore = true;3738 handleLoadMore = () => {39 if (this.loading || !this.hasMore) return;40 this.loading = true;41 setTimeout(() => {42 const newRows = generateInfiniteScrollData(this.rows.length, BATCH_SIZE) as Row[];43 this.rows = [...this.rows, ...newRows];44 if (this.rows.length >= MAX_ROWS) this.hasMore = false;45 this.loading = false;46 }, 500);47 };48}495051// infinite-scroll.demo-data.ts52// Self-contained demo table setup for this example.53import type { AngularColumnDef, Row } from "@simple-table/angular";545556const FIRST_NAMES = ["Elena", "Kai", "Amara", "Santiago", "Priya", "Magnus", "Zara", "Luca", "Sarah", "Olumide", "Isabella", "Dmitri"];57const LAST_NAMES = ["Vasquez", "Tanaka", "Okafor", "Rodriguez", "Chakraborty", "Eriksson", "Al-Rashid", "Rossi", "Kim", "Adebayo", "Chen", "Volkov"];58const DEPARTMENTS = ["Engineering", "AI Research", "UX Design", "DevOps", "Marketing", "Product", "Sales", "Finance"];5960export function generateInfiniteScrollData(startIndex: number, count: number): Row[] {61 const rows: Row[] = [];62 for (let i = 0; i < count; i++) {63 const idx = startIndex + i;64 const first = FIRST_NAMES[idx % FIRST_NAMES.length];65 const last = LAST_NAMES[idx % LAST_NAMES.length];66 rows.push({67 id: idx + 1,68 name: `${first} ${last}`,69 email: `${first.toLowerCase()}.${last.toLowerCase()}@techcorp.com`,70 department: DEPARTMENTS[idx % DEPARTMENTS.length],71 salary: 60000 + Math.floor(((idx * 7919) % 100000)),72 });73 }74 return rows;75}7677export const infiniteScrollHeaders: AngularColumnDef[] = [78 { accessor: "id", label: "ID", width: 80, type: "number" },79 { accessor: "name", label: "Name", width: "1fr", minWidth: 120 },80 { accessor: "email", label: "Email", width: 250 },81 { accessor: "department", label: "Department", width: 150 },82 {83 accessor: "salary",84 label: "Salary",85 width: 120,86 type: "number",87 align: "right",88 valueFormatter: ({ value }) => `$${(value as number).toLocaleString()}`,89 },90];9192export const infiniteScrollConfig = {93 headers: infiniteScrollHeaders,94 rows: generateInfiniteScrollData(0, 30),95} as const;96
Svelte
Copy
1<script lang="ts">2 import {SimpleTable} from "@simple-table/svelte"; import type { Theme, Row } from "@simple-table/svelte";3 import { infiniteScrollConfig, generateInfiniteScrollData } from "./infinite-scroll.demo-data";4 import "@simple-table/svelte/styles.css";56 let { height = "400px", theme }: { height?: string | number; theme?: Theme } = $props();78 const MAX_ROWS = 200;9 const BATCH_SIZE = 15;1011 let rows = $state<Row[]>(generateInfiniteScrollData(0, 30) as Row[]);12 let loading = $state(false);13 let hasMore = $state(true);1415 function handleLoadMore() {16 if (loading || !hasMore) return;17 loading = true;18 setTimeout(() => {19 const newRows = generateInfiniteScrollData(rows.length, BATCH_SIZE) as Row[];20 rows = [...rows, ...newRows];21 if (rows.length >= MAX_ROWS) hasMore = false;22 loading = false;23 }, 500);24 }25</script>2627<div>28 <div style="margin-bottom: 8px; font-size: 13px; color: #666">29 {rows.length} rows loaded{hasMore ? "" : " (all loaded)"}30 </div>31 <SimpleTable32 columns={infiniteScrollConfig.headers}33 {rows}34 isLoading={loading}35 onLoadMore={handleLoadMore}36 {height}37 {theme}38 />39</div>
Solid TSX
Copy
1import { createSignal, createMemo } from "solid-js";2import {SimpleTable} from "@simple-table/solid";import type { Theme, Row } from "@simple-table/solid";3import { infiniteScrollConfig, generateInfiniteScrollData } from "./infinite-scroll.demo-data";4import "@simple-table/solid/styles.css";56const MAX_ROWS = 200;7const BATCH_SIZE = 15;89export default function InfiniteScrollDemo(props: { height?: string | number; theme?: Theme }) {10 const [rows, setRows] = createSignal<Row[]>(generateInfiniteScrollData(0, 30) as Row[]);11 const [loading, setLoading] = createSignal(false);12 const [hasMore, setHasMore] = createSignal(true);1314 const statusText = createMemo(() =>15 `${rows().length} rows loaded${hasMore() ? "" : " (all loaded)"}`16 );1718 const handleLoadMore = () => {19 if (loading() || !hasMore()) return;20 setLoading(true);21 setTimeout(() => {22 setRows((prev) => {23 const newRows = generateInfiniteScrollData(prev.length, BATCH_SIZE) as Row[];24 const updated = [...prev, ...newRows];25 if (updated.length >= MAX_ROWS) setHasMore(false);26 return updated;27 });28 setLoading(false);29 }, 500);30 };3132 return (33 <div>34 <div style={{ "margin-bottom": "8px", "font-size": "13px", color: "#666" }}>35 {statusText()}36 </div>37 <SimpleTable38 columns={infiniteScrollConfig.headers}39 rows={rows()}40 onLoadMore={handleLoadMore}41 isLoading={loading()}42 height={props.height ?? "400px"}43 theme={props.theme}44 />45 </div>46 );47}
TypeScriptInfiniteScrollDemo.ts
Copy
1import { SimpleTableVanilla } from "simple-table-core";2import type { Theme, Row } from "simple-table-core";3import { infiniteScrollConfig, generateInfiniteScrollData } from "./infinite-scroll.demo-data";4import "simple-table-core/styles.css";56const MAX_ROWS = 200;7const BATCH_SIZE = 15;89export function renderInfiniteScrollDemo(10 container: HTMLElement,11 options?: { height?: string | number; theme?: Theme }12): SimpleTableVanilla {13 const wrapper = document.createElement("div");1415 const status = document.createElement("div");16 Object.assign(status.style, { marginBottom: "8px", fontSize: "13px", color: "#666" });1718 wrapper.appendChild(status);1920 const tableContainer = document.createElement("div");21 wrapper.appendChild(tableContainer);22 container.appendChild(wrapper);2324 let rows: Row[] = generateInfiniteScrollData(0, 30) as Row[];25 let loading = false;26 let hasMore = true;2728 const updateStatus = () => {29 status.textContent = `${rows.length} rows loaded${hasMore ? "" : " (all loaded)"}`;30 };31 updateStatus();3233 const table = new SimpleTableVanilla(tableContainer, {34 columns: infiniteScrollConfig.headers,35 rows,36 height: options?.height ?? "400px",37 theme: options?.theme,38 onLoadMore: () => {39 if (loading || !hasMore) return;40 loading = true;41 table.update({ isLoading: true });42 setTimeout(() => {43 const newRows = generateInfiniteScrollData(rows.length, BATCH_SIZE) as Row[];44 rows = [...rows, ...newRows];45 if (rows.length >= MAX_ROWS) hasMore = false;46 loading = false;47 table.update({ rows, isLoading: false });48 updateStatus();49 }, 500);50 },51 });5253 return table;54}555657// infinite-scroll.demo-data.ts58// Self-contained demo table setup for this example.59import type { ColumnDef, Row } from "simple-table-core";606162const FIRST_NAMES = ["Elena", "Kai", "Amara", "Santiago", "Priya", "Magnus", "Zara", "Luca", "Sarah", "Olumide", "Isabella", "Dmitri"];63const LAST_NAMES = ["Vasquez", "Tanaka", "Okafor", "Rodriguez", "Chakraborty", "Eriksson", "Al-Rashid", "Rossi", "Kim", "Adebayo", "Chen", "Volkov"];64const DEPARTMENTS = ["Engineering", "AI Research", "UX Design", "DevOps", "Marketing", "Product", "Sales", "Finance"];6566export function generateInfiniteScrollData(startIndex: number, count: number): Row[] {67 const rows: Row[] = [];68 for (let i = 0; i < count; i++) {69 const idx = startIndex + i;70 const first = FIRST_NAMES[idx % FIRST_NAMES.length];71 const last = LAST_NAMES[idx % LAST_NAMES.length];72 rows.push({73 id: idx + 1,74 name: `${first} ${last}`,75 email: `${first.toLowerCase()}.${last.toLowerCase()}@techcorp.com`,76 department: DEPARTMENTS[idx % DEPARTMENTS.length],77 salary: 60000 + Math.floor(((idx * 7919) % 100000)),78 });79 }80 return rows;81}8283export const infiniteScrollHeaders: ColumnDef[] = [84 { accessor: "id", label: "ID", width: 80, type: "number" },85 { accessor: "name", label: "Name", width: "1fr", minWidth: 120 },86 { accessor: "email", label: "Email", width: 250 },87 { accessor: "department", label: "Department", width: 150 },88 {89 accessor: "salary",90 label: "Salary",91 width: 120,92 type: "number",93 align: "right",94 valueFormatter: ({ value }) => `$${(value as number).toLocaleString()}`,95 },96];9798export const infiniteScrollConfig = {99 headers: infiniteScrollHeaders,100 rows: generateInfiniteScrollData(0, 30),101} as const;102
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. |