Documentation
Footer Renderer
Replace the default pagination footer with your own UI via footerRenderer.
Custom pagination footer
Pass footerRenderer with enablePagination. It replaces the default footer. Use hasPrevPage / hasNextPage to disable controls at the ends.
React TSX
Copy
const FooterBar = ({currentPage, totalPages, startRow, endRow, totalRows,hasPrevPage, hasNextPage, onPrevPage, onNextPage,}) => (<div style={{ display: "flex", justifyContent: "space-between", padding: 12 }}><span>Showing {startRow}-{endRow} of {totalRows}</span><div><button disabled={!hasPrevPage} onClick={onPrevPage}>Prev</button><span> {currentPage} / {totalPages} </span><button disabled={!hasNextPage} onClick={() => void onNextPage()}>Next</button></div></div>);<SimpleTablecolumns={columns}rows={rows}enablePaginationrowsPerPage={10}footerRenderer={FooterBar}/>
Vue SFC
Copy
import { h } from "vue";const FooterBar = (fp) =>h("div", { style: { display: "flex", justifyContent: "space-between", padding: "12px" } }, [h("span", `Showing ${fp.startRow}-${fp.endRow} of ${fp.totalRows}`),h("div", [h("button", { disabled: !fp.hasPrevPage, onClick: fp.onPrevPage }, "Prev"),h("span", ` ${fp.currentPage} / ${fp.totalPages} `),h("button", {disabled: !fp.hasNextPage,onClick: () => { void fp.onNextPage(); },}, "Next"),]),]);<SimpleTable:columns="columns":rows="rows":enable-pagination="true":rows-per-page="10":footer-renderer="FooterBar"/>
Angularfooter-bar.component.ts — @Input() currentPage, totalPages, …
Copy
// Template: Prev / page / Next wired to onPrevPage / onNextPage<simple-table[columns]="columns"[rows]="rows"[enablePagination]="true"[rowsPerPage]="10"[footerRenderer]="FooterBarComponent"></simple-table>
Svelte
Copy
<!-- FooterBar.svelte --><script lang="ts">import type { FooterRendererProps } from "@simple-table/svelte";let {currentPage, totalPages, startRow, endRow, totalRows,hasPrevPage, hasNextPage, onPrevPage, onNextPage,}: FooterRendererProps = $props();</script><div style="display:flex;justify-content:space-between;padding:12px"><span>Showing {startRow}-{endRow} of {totalRows}</span><div><button disabled={!hasPrevPage} onclick={onPrevPage}>Prev</button><span> {currentPage} / {totalPages} </span><button disabled={!hasNextPage} onclick={() => void onNextPage()}>Next</button></div></div><SimpleTable{columns}{rows}enablePagination={true}rowsPerPage={10}footerRenderer={FooterBar}/>
Solid TSX
Copy
const FooterBar = (props) => (<div style={{ display: "flex", "justify-content": "space-between", padding: "12px" }}><span>Showing {props.startRow}-{props.endRow} of {props.totalRows}</span><div><button disabled={!props.hasPrevPage} onClick={props.onPrevPage}>Prev</button><span> {props.currentPage} / {props.totalPages} </span><button disabled={!props.hasNextPage} onClick={() => void props.onNextPage()}>Next</button></div></div>);<SimpleTablecolumns={columns}rows={rows()}enablePaginationrowsPerPage={10}footerRenderer={FooterBar}/>
TypeScript
Copy
const footerRenderer = ({currentPage, totalPages, startRow, endRow, totalRows,hasPrevPage, hasNextPage, onPrevPage, onNextPage,}) => {const el = document.createElement("div");el.style.cssText = "display:flex;justify-content:space-between;padding:12px";el.innerHTML = `<span>Showing ${startRow}-${endRow} of ${totalRows}</span><div><button ${!hasPrevPage ? "disabled" : ""} data-prev>Prev</button><span> ${currentPage} / ${totalPages} </span><button ${!hasNextPage ? "disabled" : ""} data-next>Next</button></div>`;el.querySelector("[data-prev]")?.addEventListener("click", onPrevPage);el.querySelector("[data-next]")?.addEventListener("click", () => { void onNextPage(); });return el;};new SimpleTableVanilla(container, {columns,rows,enablePagination: true,rowsPerPage: 10,footerRenderer,});
Footer above the table
Set footerPosition="top" for the default or custom footer. Defaults to "bottom".
React TSX
Copy
<SimpleTablecolumns={columns}rows={rows}enablePaginationfooterPosition="top"footerRenderer={FooterBar}/>
Vue SFC
Copy
<SimpleTable:columns="columns":rows="rows":enable-pagination="true"footer-position="top":footer-renderer="FooterBar"/>
Angular
Copy
<simple-table[columns]="columns"[rows]="rows"[enablePagination]="true"footerPosition="top"[footerRenderer]="FooterBarComponent"></simple-table>
Svelte
Copy
<SimpleTable{columns}{rows}enablePagination={true}footerPosition="top"footerRenderer={FooterBar}/>
Solid TSX
Copy
<SimpleTablecolumns={columns}rows={rows()}enablePaginationfooterPosition="top"footerRenderer={FooterBar}/>
TypeScript
Copy
new SimpleTableVanilla(container, {columns,rows,enablePagination: true,footerPosition: "top",footerRenderer,});
Notes
Page numbers are 1-based. onNextPage is async. For pagination setup without a custom footer, see Pagination.
Example
Custom footer with page info and navigation. Use Code or StackBlitz for the full example.
React TSX
Copy
1import {SimpleTable} from "@simple-table/react";import type { Theme, FooterRendererProps } from "@simple-table/react";2import { footerRendererConfig } from "./footer-renderer.demo-data";3import "@simple-table/react/styles.css";45function getFooterColors(theme?: Theme) {6 const isModernDark = theme === "modern-dark";7 const isDark = theme === "dark" || isModernDark;8 const isModernLight = theme === "modern-light";9 const isLight = theme === "light" || isModernLight;1011 if (isModernDark)12 return {13 background: "#1f2937", border: "#374151", text: "#d1d5db",14 buttonBg: "#374151", buttonBorder: "#4b5563", buttonActive: "#3b82f6",15 buttonText: "#d1d5db", buttonDisabled: "#6b7280",16 };17 if (isDark)18 return {19 background: "#1f2937", border: "#374151", text: "#e5e7eb",20 buttonBg: "#374151", buttonBorder: "#4b5563", buttonActive: "#3b82f6",21 buttonText: "#d1d5db", buttonDisabled: "#6b7280",22 };23 if (isLight)24 return {25 background: "white", border: "#f3f4f6", text: "#6b7280",26 buttonBg: "white", buttonBorder: "#e5e7eb", buttonActive: "#3b82f6",27 buttonText: "#374151", buttonDisabled: "#d1d5db",28 };29 return {30 background: "#f8fafc", border: "#e2e8f0", text: "#475569",31 buttonBg: "white", buttonBorder: "#e2e8f0", buttonActive: "#3b82f6",32 buttonText: "#64748b", buttonDisabled: "#cbd5e1",33 };34}3536const FooterRendererDemo = ({37 height = "400px",38 theme,39}: {40 height?: string | number;41 theme?: Theme;42}) => {43 const c = getFooterColors(theme);4445 return (46 <SimpleTable47 columns={footerRendererConfig.headers}48 rows={footerRendererConfig.rows}49 enablePagination={true}50 rowsPerPage={10}51 height={height}52 theme={theme}53 footerRenderer={({54 currentPage,55 startRow,56 endRow,57 totalRows,58 totalPages,59 hasPrevPage,60 hasNextPage,61 onPrevPage,62 onNextPage,63 onPageChange,64 }: FooterRendererProps) => (65 <div66 style={{67 display: "flex",68 alignItems: "center",69 justifyContent: "space-between",70 padding: "16px 20px",71 backgroundColor: c.background,72 borderTop: `2px solid ${c.border}`,73 }}74 >75 <div style={{ display: "flex", alignItems: "center", gap: "12px" }}>76 <span style={{ fontSize: "14px", fontWeight: 600, color: c.text }}>77 Showing {startRow}-{endRow} of {totalRows} items78 </span>79 </div>8081 <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>82 <button83 onClick={onPrevPage}84 disabled={!hasPrevPage}85 style={{86 padding: "8px 16px", fontSize: "14px", fontWeight: 500,87 color: hasPrevPage ? c.buttonActive : c.buttonDisabled,88 backgroundColor: c.buttonBg, border: `1px solid ${c.buttonBorder}`,89 borderRadius: "6px", cursor: hasPrevPage ? "pointer" : "not-allowed",90 transition: "all 0.2s",91 }}92 >93 Previous94 </button>9596 <div style={{ display: "flex", gap: "4px" }}>97 {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (98 <button99 key={page}100 onClick={() => onPageChange(page)}101 style={{102 padding: "8px 12px", fontSize: "14px", fontWeight: 500,103 color: currentPage === page ? "white" : c.buttonText,104 backgroundColor: currentPage === page ? c.buttonActive : c.buttonBg,105 border: `1px solid ${c.buttonBorder}`, borderRadius: "6px",106 cursor: "pointer", transition: "all 0.2s", minWidth: "40px",107 }}108 >109 {page}110 </button>111 ))}112 </div>113114 <button115 onClick={onNextPage}116 disabled={!hasNextPage}117 style={{118 padding: "8px 16px", fontSize: "14px", fontWeight: 500,119 color: hasNextPage ? c.buttonActive : c.buttonDisabled,120 backgroundColor: c.buttonBg, border: `1px solid ${c.buttonBorder}`,121 borderRadius: "6px", cursor: hasNextPage ? "pointer" : "not-allowed",122 transition: "all 0.2s",123 }}124 >125 Next126 </button>127 </div>128 </div>129 )}130 />131 );132};133134export default FooterRendererDemo;
Vue SFC
Copy
1<script setup lang="ts">2import { defineComponent, h } from "vue";3import { SimpleTable } from "@simple-table/vue";4import type { Theme, FooterRendererProps } from "@simple-table/vue";5import { footerRendererConfig } from "./footer-renderer.demo-data";6import "@simple-table/vue/styles.css";78const props = withDefaults(defineProps<{ height?: string | number; theme?: Theme }>(), {9 height: "400px",10});1112function getFooterColors(theme?: Theme) {13 switch (theme) {14 case "modern-dark":15 case "dark":16 return {17 background: "#1f2937",18 border: "#374151",19 text: "#d1d5db",20 buttonBg: "#374151",21 buttonBorder: "#4b5563",22 buttonActive: "#3b82f6",23 buttonText: "#d1d5db",24 buttonDisabled: "#6b7280",25 };26 case "light":27 case "modern-light":28 return {29 background: "white",30 border: "#f3f4f6",31 text: "#6b7280",32 buttonBg: "white",33 buttonBorder: "#e5e7eb",34 buttonActive: "#3b82f6",35 buttonText: "#374151",36 buttonDisabled: "#d1d5db",37 };38 default:39 return {40 background: "#f8fafc",41 border: "#e2e8f0",42 text: "#475569",43 buttonBg: "white",44 buttonBorder: "#e2e8f0",45 buttonActive: "#3b82f6",46 buttonText: "#64748b",47 buttonDisabled: "#cbd5e1",48 };49 }50}5152const FooterBar = defineComponent({53 name: "FooterBar",54 setup(fp: FooterRendererProps) {55 return () => {56 const c = getFooterColors(props.theme);57 const btnStyle = (disabled: boolean, active = false) => ({58 padding: "8px 16px",59 fontSize: "14px",60 fontWeight: "500",61 color: active ? "white" : disabled ? c.buttonDisabled : c.buttonActive,62 backgroundColor: active ? c.buttonActive : c.buttonBg,63 border: `1px solid ${c.buttonBorder}`,64 borderRadius: "6px",65 cursor: disabled ? ("not-allowed" as const) : ("pointer" as const),66 transition: "all 0.2s",67 minWidth: "40px",68 });69 return h(70 "div",71 {72 style: {73 display: "flex",74 alignItems: "center",75 justifyContent: "space-between",76 padding: "16px 20px",77 backgroundColor: c.background,78 borderTop: `2px solid ${c.border}`,79 },80 },81 [82 h(83 "span",84 { style: { fontSize: "14px", fontWeight: "600", color: c.text } },85 `Showing ${fp.startRow}–${fp.endRow} of ${fp.totalRows} items`,86 ),87 h(88 "div",89 { style: { display: "flex", alignItems: "center", gap: "8px" } },90 [91 h("button", {92 style: btnStyle(!fp.hasPrevPage),93 disabled: !fp.hasPrevPage,94 onClick: fp.onPrevPage,95 }, "Previous"),96 h(97 "div",98 { style: { display: "flex", gap: "4px" } },99 Array.from({ length: fp.totalPages }, (_, i) => i + 1).map((page) =>100 h("button", {101 key: page,102 style: { ...btnStyle(false, page === fp.currentPage), padding: "8px 12px" },103 onClick: () => fp.onPageChange(page),104 }, String(page)),105 ),106 ),107 h("button", {108 style: btnStyle(!fp.hasNextPage),109 disabled: !fp.hasNextPage,110 onClick: () => {111 void fp.onNextPage();112 },113 }, "Next"),114 ],115 ),116 ],117 );118 };119 },120});121</script>122123<template>124 <SimpleTable125 :columns="footerRendererConfig.headers"126 :rows="footerRendererConfig.rows"127 :footer-renderer="FooterBar"128 :enable-pagination="true"129 :rows-per-page="10"130 :hide-footer="false"131 :height="props.height"132 :theme="props.theme"133 />134</template>
Angularfooter-demo-theme-context.ts
Copy
1import { signal } from "@angular/core";2import type { Theme } from "@simple-table/angular";34export const footerDemoThemeContext = signal<Theme | undefined>(undefined);567// footer-pagination.component.ts8import { Component, computed, Input } from "@angular/core";9import type { FooterRendererProps } from "@simple-table/angular";10import type { Theme } from "@simple-table/angular";11import { footerDemoThemeContext } from "./footer-demo-theme-context";1213function palette(theme?: Theme) {14 switch (theme) {15 case "modern-dark":16 case "dark":17 return {18 background: "#1f2937",19 border: "#374151",20 text: "#d1d5db",21 buttonBg: "#374151",22 buttonBorder: "#4b5563",23 buttonActive: "#3b82f6",24 buttonText: "#d1d5db",25 buttonDisabled: "#6b7280",26 };27 case "light":28 case "modern-light":29 return {30 background: "white",31 border: "#f3f4f6",32 text: "#6b7280",33 buttonBg: "white",34 buttonBorder: "#e5e7eb",35 buttonActive: "#3b82f6",36 buttonText: "#374151",37 buttonDisabled: "#d1d5db",38 };39 default:40 return {41 background: "#f8fafc",42 border: "#e2e8f0",43 text: "#475569",44 buttonBg: "white",45 buttonBorder: "#e2e8f0",46 buttonActive: "#3b82f6",47 buttonText: "#64748b",48 buttonDisabled: "#cbd5e1",49 };50 }51}5253@Component({54 standalone: true,55 selector: "demo-footer-pagination",56 template: `57 <div58 style="display:flex;align-items:center;justify-content:space-between;padding:16px 20px;"59 [style.background-color]="colors().background"60 [style.border-top]="'2px solid ' + colors().border"61 >62 <span style="font-size:14px;font-weight:600;" [style.color]="colors().text">63 Showing {{ startRow }}–{{ endRow }} of {{ totalRows }} items64 </span>65 <div style="display:flex;align-items:center;gap:8px;">66 <button67 type="button"68 [disabled]="!hasPrevPage"69 (click)="onPrevPage()"70 [style.padding]="'8px 16px'"71 [style.font-size]="'14px'"72 [style.font-weight]="'500'"73 [style.color]="btnStylePrev().color"74 [style.background-color]="btnStylePrev().bg"75 [style.border]="'1px solid ' + colors().buttonBorder"76 [style.border-radius]="'6px'"77 [style.cursor]="hasPrevPage ? 'pointer' : 'not-allowed'"78 [style.min-width]="'40px'"79 >80 Previous81 </button>82 <div style="display:flex;gap:4px;">83 @for (p of pageNumbers(); track p) {84 <button85 type="button"86 (click)="onPageChange(p)"87 [style.padding]="'8px 12px'"88 [style.font-size]="'14px'"89 [style.font-weight]="'500'"90 [style.color]="pageBtnStyle(p).color"91 [style.background-color]="pageBtnStyle(p).bg"92 [style.border]="'1px solid ' + colors().buttonBorder"93 [style.border-radius]="'6px'"94 style="cursor:pointer;min-width:40px;"95 >96 {{ p }}97 </button>98 }99 </div>100 <button101 type="button"102 [disabled]="!hasNextPage"103 (click)="onNext()"104 [style.padding]="'8px 16px'"105 [style.font-size]="'14px'"106 [style.font-weight]="'500'"107 [style.color]="btnStyleNext().color"108 [style.background-color]="btnStyleNext().bg"109 [style.border]="'1px solid ' + colors().buttonBorder"110 [style.border-radius]="'6px'"111 [style.cursor]="hasNextPage ? 'pointer' : 'not-allowed'"112 [style.min-width]="'40px'"113 >114 Next115 </button>116 </div>117 </div>118 `,119})120export class FooterPaginationComponent {121 @Input({ required: true }) currentPage!: FooterRendererProps["currentPage"];122 @Input({ required: true }) endRow!: FooterRendererProps["endRow"];123 @Input({ required: true }) hasNextPage!: FooterRendererProps["hasNextPage"];124 @Input({ required: true }) hasPrevPage!: FooterRendererProps["hasPrevPage"];125 @Input({ required: true }) onNextPage!: FooterRendererProps["onNextPage"];126 @Input({ required: true }) onPageChange!: FooterRendererProps["onPageChange"];127 @Input({ required: true }) onPrevPage!: FooterRendererProps["onPrevPage"];128 @Input({ required: true }) rowsPerPage!: FooterRendererProps["rowsPerPage"];129 @Input({ required: true }) startRow!: FooterRendererProps["startRow"];130 @Input({ required: true }) totalPages!: FooterRendererProps["totalPages"];131 @Input({ required: true }) totalRows!: FooterRendererProps["totalRows"];132133 readonly colors = computed(() => palette(footerDemoThemeContext()));134135 pageNumbers(): number[] {136 return Array.from({ length: this.totalPages }, (_, i) => i + 1);137 }138139 btnStylePrev(): { color: string; bg: string } {140 const c = this.colors();141 return {142 color: !this.hasPrevPage ? c.buttonDisabled : c.buttonActive,143 bg: c.buttonBg,144 };145 }146147 btnStyleNext(): { color: string; bg: string } {148 const c = this.colors();149 return {150 color: !this.hasNextPage ? c.buttonDisabled : c.buttonActive,151 bg: c.buttonBg,152 };153 }154155 pageBtnStyle(p: number): { color: string; bg: string } {156 const c = this.colors();157 const active = p === this.currentPage;158 return {159 color: active ? "white" : c.buttonActive,160 bg: active ? c.buttonActive : c.buttonBg,161 };162 }163164 onNext(): void {165 void this.onNextPage();166 }167}168169170// footer-renderer-demo.component.ts171import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from "@angular/core";172import { SimpleTableComponent } from "@simple-table/angular";173import type { AngularColumnDef, Row, Theme } from "@simple-table/angular";174import { footerDemoThemeContext } from "./footer-demo-theme-context";175import { FooterPaginationComponent } from "./footer-pagination.component";176import { footerRendererConfig } from "./footer-renderer.demo-data";177import "@simple-table/angular/styles.css";178179@Component({180 selector: "footer-renderer-demo",181 standalone: true,182 imports: [SimpleTableComponent],183 template: `184 <simple-table185 [rows]="rows"186 [columns]="headers"187 [footerRenderer]="footerRenderer"188 [enablePagination]="true"189 [rowsPerPage]="10"190 [hideFooter]="false"191 [height]="height"192 [theme]="theme"193 ></simple-table>194 `,195})196export class FooterRendererDemoComponent implements OnInit, OnChanges, OnDestroy {197 @Input() height: string | number = "400px";198 @Input() theme?: Theme;199200 readonly rows: Row[] = footerRendererConfig.rows;201 readonly headers: AngularColumnDef[] = footerRendererConfig.headers;202 readonly footerRenderer = FooterPaginationComponent;203204 ngOnInit(): void {205 footerDemoThemeContext.set(this.theme);206 }207208 ngOnChanges(changes: SimpleChanges): void {209 if (changes["theme"]) {210 footerDemoThemeContext.set(this.theme);211 }212 }213214 ngOnDestroy(): void {215 footerDemoThemeContext.set(undefined);216 }217}218219220// footer-renderer.demo-data.ts221// Self-contained demo table setup for this example.222import type { AngularColumnDef, Row } from "@simple-table/angular";223224225export const footerRendererHeaders: AngularColumnDef[] = [226 { accessor: "id", label: "ID", width: 60, type: "number" },227 { accessor: "product", label: "Product Name", width: 220, type: "string" },228 { accessor: "category", label: "Category", width: 150, type: "string" },229 { accessor: "price", label: "Price", width: 100, type: "number" },230 { accessor: "stock", label: "Stock", width: 100, type: "number" },231 { accessor: "status", label: "Status", width: "1fr", type: "string" },232];233234export const footerRendererData: Row[] = [235 { id: 1, product: "MacBook Pro 16-inch M3 Max", category: "Laptops", price: 3499, stock: 28, status: "In Stock" },236 { id: 2, product: "Dell XPS 15 OLED Touchscreen", category: "Laptops", price: 2299, stock: 42, status: "In Stock" },237 { id: 3, product: "ThinkPad X1 Carbon Gen 11", category: "Laptops", price: 1899, stock: 35, status: "In Stock" },238 { id: 4, product: "HP Spectre x360 Convertible", category: "Laptops", price: 1649, stock: 51, status: "In Stock" },239 { id: 5, product: "ASUS ROG Strix Gaming Laptop", category: "Laptops", price: 2199, stock: 19, status: "In Stock" },240 { id: 6, product: "Logitech MX Master 3S Wireless", category: "Accessories", price: 99, stock: 342, status: "In Stock" },241 { id: 7, product: "Apple Magic Mouse Black", category: "Accessories", price: 89, stock: 218, status: "In Stock" },242 { id: 8, product: "Razer DeathAdder V3 Pro", category: "Accessories", price: 149, stock: 167, status: "In Stock" },243 { id: 9, product: "Microsoft Surface Precision Mouse", category: "Accessories", price: 79, stock: 203, status: "In Stock" },244 { id: 10, product: "Corsair K95 RGB Platinum XT", category: "Keyboards", price: 199, stock: 89, status: "In Stock" },245 { id: 11, product: "Keychron Q1 Pro Mechanical", category: "Keyboards", price: 189, stock: 134, status: "In Stock" },246 { id: 12, product: "Ducky One 3 TKL RGB", category: "Keyboards", price: 159, stock: 76, status: "In Stock" },247 { id: 13, product: "Leopold FC900R PD Cherry MX", category: "Keyboards", price: 169, stock: 54, status: "In Stock" },248 { id: 14, product: "LG UltraGear 27-inch 4K 144Hz", category: "Monitors", price: 799, stock: 31, status: "In Stock" },249 { id: 15, product: "Samsung Odyssey G9 Curved", category: "Monitors", price: 1299, stock: 18, status: "In Stock" },250 { id: 16, product: "Dell UltraSharp U2723DE 27in", category: "Monitors", price: 649, stock: 47, status: "In Stock" },251 { id: 17, product: "BenQ PD3220U Designer 32in", category: "Monitors", price: 1099, stock: 23, status: "In Stock" },252 { id: 18, product: "ASUS ProArt Display PA279CRV", category: "Monitors", price: 549, stock: 39, status: "In Stock" },253 { id: 19, product: "Sony WH-1000XM5 Noise Cancelling", category: "Audio", price: 399, stock: 127, status: "In Stock" },254 { id: 20, product: "Bose QuietComfort Ultra", category: "Audio", price: 429, stock: 98, status: "In Stock" },255 { id: 21, product: "Apple AirPods Max Space Gray", category: "Audio", price: 549, stock: 82, status: "In Stock" },256 { id: 22, product: "Sennheiser Momentum 4 Wireless", category: "Audio", price: 349, stock: 104, status: "In Stock" },257 { id: 23, product: "Logitech C920 HD Pro Webcam", category: "Video", price: 79, stock: 245, status: "In Stock" },258 { id: 24, product: "Elgato Facecam Pro 4K60", category: "Video", price: 299, stock: 67, status: "In Stock" },259 { id: 25, product: "Razer Kiyo Pro Ultra 4K", category: "Video", price: 329, stock: 41, status: "In Stock" },260 { id: 26, product: "Herman Miller Aeron Ergonomic", category: "Furniture", price: 1695, stock: 12, status: "Low Stock" },261 { id: 27, product: "Steelcase Leap V2 Office Chair", category: "Furniture", price: 1299, stock: 15, status: "In Stock" },262 { id: 28, product: "Autonomous SmartDesk Pro", category: "Furniture", price: 899, stock: 28, status: "In Stock" },263 { id: 29, product: "Uplift V2 Standing Desk Frame", category: "Furniture", price: 749, stock: 34, status: "In Stock" },264 { id: 30, product: "FlexiSpot E7 Plus Adjustable", category: "Furniture", price: 649, stock: 45, status: "In Stock" },265 { id: 31, product: "Anker PowerCore 20000mAh", category: "Power", price: 49, stock: 412, status: "In Stock" },266 { id: 32, product: "RAVPower 60W USB-C Charger", category: "Power", price: 39, stock: 387, status: "In Stock" },267 { id: 33, product: "Belkin BoostCharge Pro 3-in-1", category: "Power", price: 149, stock: 156, status: "In Stock" },268 { id: 34, product: "Samsung T7 Shield 2TB SSD", category: "Storage", price: 199, stock: 234, status: "In Stock" },269 { id: 35, product: "SanDisk Extreme Pro 1TB Portable", category: "Storage", price: 159, stock: 189, status: "In Stock" },270 { id: 36, product: "WD My Passport 5TB External", category: "Storage", price: 139, stock: 276, status: "In Stock" },271 { id: 37, product: "Crucial X9 Pro 4TB Rugged", category: "Storage", price: 289, stock: 143, status: "In Stock" },272 { id: 38, product: "CalDigit TS4 Thunderbolt 4 Dock", category: "Hubs & Docks", price: 399, stock: 52, status: "In Stock" },273 { id: 39, product: "Anker 577 Thunderbolt Docking", category: "Hubs & Docks", price: 299, stock: 78, status: "In Stock" },274 { id: 40, product: "HyperDrive Gen2 16-Port USB-C", category: "Hubs & Docks", price: 249, stock: 91, status: "In Stock" },275 { id: 41, product: "Blue Yeti X Professional USB", category: "Audio", price: 169, stock: 123, status: "In Stock" },276 { id: 42, product: "Shure MV7 Podcast Microphone", category: "Audio", price: 249, stock: 87, status: "In Stock" },277 { id: 43, product: "Elgato Wave:3 Premium USB", category: "Audio", price: 159, stock: 104, status: "In Stock" },278 { id: 44, product: "Rode NT-USB Mini Studio", category: "Audio", price: 99, stock: 167, status: "In Stock" },279 { id: 45, product: "Audio-Technica AT2020USB+", category: "Audio", price: 149, stock: 145, status: "In Stock" },280];281282export const footerRendererConfig = {283 headers: footerRendererHeaders,284 rows: footerRendererData,285 tableProps: {286 enablePagination: true,287 rowsPerPage: 10,288 },289} as const;290
SvelteFooterDemoBar.svelte
Copy
1<script lang="ts">2 import { get } from "svelte/store";3 import type { FooterRendererProps, Theme } from "@simple-table/svelte";4 import { footerDemoTheme } from "./footer-demo-theme";56 let fp: FooterRendererProps = $props();78 function getFooterColors(t?: Theme) {9 switch (t) {10 case "modern-dark":11 case "dark":12 return {13 background: "#1f2937",14 border: "#374151",15 text: "#d1d5db",16 buttonBg: "#374151",17 buttonBorder: "#4b5563",18 buttonActive: "#3b82f6",19 buttonDisabled: "#6b7280",20 };21 case "light":22 case "modern-light":23 return {24 background: "white",25 border: "#f3f4f6",26 text: "#6b7280",27 buttonBg: "white",28 buttonBorder: "#e5e7eb",29 buttonActive: "#3b82f6",30 buttonDisabled: "#d1d5db",31 };32 default:33 return {34 background: "#f8fafc",35 border: "#e2e8f0",36 text: "#475569",37 buttonBg: "white",38 buttonBorder: "#e2e8f0",39 buttonActive: "#3b82f6",40 buttonDisabled: "#cbd5e1",41 };42 }43 }4445 let c = $state(getFooterColors(get(footerDemoTheme)));46 $effect(() => {47 c = getFooterColors($footerDemoTheme);48 });4950 function btnStyle(disabled: boolean, active = false): string {51 const color = active ? "white" : disabled ? c.buttonDisabled : c.buttonActive;52 const bg = active ? c.buttonActive : c.buttonBg;53 return [54 `padding:8px 16px`,55 `font-size:14px`,56 `font-weight:500`,57 `color:${color}`,58 `background-color:${bg}`,59 `border:1px solid ${c.buttonBorder}`,60 `border-radius:6px`,61 `cursor:${disabled ? "not-allowed" : "pointer"}`,62 `transition:all 0.2s`,63 `min-width:40px`,64 ].join(";");65 }66</script>6768<div69 style="display:flex;align-items:center;justify-content:space-between;padding:16px 20px;background-color:{c.background};border-top:2px solid {c.border};"70>71 <span style="font-size:14px;font-weight:600;color:{c.text};">72 Showing {fp.startRow}–{fp.endRow} of {fp.totalRows} items73 </span>74 <div style="display:flex;align-items:center;gap:8px;">75 <button type="button" style={btnStyle(!fp.hasPrevPage)} disabled={!fp.hasPrevPage} onclick={() => fp.onPrevPage()}>76 Previous77 </button>78 <div style="display:flex;gap:4px;">79 {#each Array.from({ length: fp.totalPages }, (_, i) => i + 1) as page (page)}80 <button81 type="button"82 style="{btnStyle(false, page === fp.currentPage)};padding:8px 12px;"83 onclick={() => fp.onPageChange(page)}84 >85 {page}86 </button>87 {/each}88 </div>89 <button90 type="button"91 style={btnStyle(!fp.hasNextPage)}92 disabled={!fp.hasNextPage}93 onclick={() => void fp.onNextPage()}94 >95 Next96 </button>97 </div>98</div>99100101// FooterRendererDemo.svelte102<script lang="ts">103 import { SimpleTable } from "@simple-table/svelte";104 import type { Theme } from "@simple-table/svelte";105 import { footerRendererConfig } from "./footer-renderer.demo-data";106 import { footerDemoTheme } from "./footer-demo-theme";107 import FooterDemoBar from "./FooterDemoBar.svelte";108 import "@simple-table/svelte/styles.css";109110 let { height = "400px", theme }: { height?: string | number; theme?: Theme } = $props();111112 $effect.pre(() => {113 footerDemoTheme.set(theme);114 });115</script>116117<SimpleTable118 columns={footerRendererConfig.headers}119 rows={footerRendererConfig.rows}120 footerRenderer={FooterDemoBar}121 enablePagination={true}122 rowsPerPage={10}123 hideFooter={false}124 {height}125 {theme}126/>127
Solid TSX
Copy
1import {SimpleTable} from "@simple-table/solid";import type { Theme, FooterRendererProps } from "@simple-table/solid";2import { footerRendererConfig } from "./footer-renderer.demo-data";3import { For } from "solid-js";4import "@simple-table/solid/styles.css";56function getFooterColors(theme?: Theme) {7 const isModernDark = theme === "modern-dark";8 const isDark = theme === "dark" || isModernDark;9 const isModernLight = theme === "modern-light";10 const isLight = theme === "light" || isModernLight;1112 if (isModernDark)13 return {14 background: "#1f2937", border: "#374151", text: "#d1d5db",15 buttonBg: "#374151", buttonBorder: "#4b5563", buttonActive: "#3b82f6",16 buttonText: "#d1d5db", buttonDisabled: "#6b7280",17 };18 if (isDark)19 return {20 background: "#1f2937", border: "#374151", text: "#e5e7eb",21 buttonBg: "#374151", buttonBorder: "#4b5563", buttonActive: "#3b82f6",22 buttonText: "#d1d5db", buttonDisabled: "#6b7280",23 };24 if (isLight)25 return {26 background: "white", border: "#f3f4f6", text: "#6b7280",27 buttonBg: "white", buttonBorder: "#e5e7eb", buttonActive: "#3b82f6",28 buttonText: "#374151", buttonDisabled: "#d1d5db",29 };30 return {31 background: "#f8fafc", border: "#e2e8f0", text: "#475569",32 buttonBg: "white", buttonBorder: "#e2e8f0", buttonActive: "#3b82f6",33 buttonText: "#64748b", buttonDisabled: "#cbd5e1",34 };35}3637export default function FooterRendererDemo(props: { height?: string | number; theme?: Theme }) {38 const c = getFooterColors(props.theme);39 const pages = (totalPages: number) => Array.from({ length: totalPages }, (_, i) => i + 1);4041 return (42 <SimpleTable43 columns={footerRendererConfig.headers}44 rows={footerRendererConfig.rows}45 enablePagination={true}46 rowsPerPage={10}47 height={props.height ?? "400px"}48 theme={props.theme}49 footerRenderer={(fp: FooterRendererProps) => (50 <div51 style={{52 display: "flex",53 "align-items": "center",54 "justify-content": "space-between",55 padding: "16px 20px",56 "background-color": c.background,57 "border-top": `2px solid ${c.border}`,58 }}59 >60 <div style={{ display: "flex", "align-items": "center", gap: "12px" }}>61 <span style={{ "font-size": "14px", "font-weight": "600", color: c.text }}>62 Showing {fp.startRow}-{fp.endRow} of {fp.totalRows} items63 </span>64 </div>6566 <div style={{ display: "flex", "align-items": "center", gap: "8px" }}>67 <button68 onClick={fp.onPrevPage}69 disabled={!fp.hasPrevPage}70 style={{71 padding: "8px 16px", "font-size": "14px", "font-weight": "500",72 color: fp.hasPrevPage ? c.buttonActive : c.buttonDisabled,73 "background-color": c.buttonBg, border: `1px solid ${c.buttonBorder}`,74 "border-radius": "6px",75 cursor: fp.hasPrevPage ? "pointer" : "not-allowed",76 transition: "all 0.2s",77 }}78 >79 Previous80 </button>8182 <div style={{ display: "flex", gap: "4px" }}>83 <For each={pages(fp.totalPages)}>84 {(page) => (85 <button86 onClick={() => fp.onPageChange(page)}87 style={{88 padding: "8px 12px", "font-size": "14px", "font-weight": "500",89 color: fp.currentPage === page ? "white" : c.buttonText,90 "background-color": fp.currentPage === page ? c.buttonActive : c.buttonBg,91 border: `1px solid ${c.buttonBorder}`, "border-radius": "6px",92 cursor: "pointer", transition: "all 0.2s", "min-width": "40px",93 }}94 >95 {page}96 </button>97 )}98 </For>99 </div>100101 <button102 onClick={() => fp.onNextPage()}103 disabled={!fp.hasNextPage}104 style={{105 padding: "8px 16px", "font-size": "14px", "font-weight": "500",106 color: fp.hasNextPage ? c.buttonActive : c.buttonDisabled,107 "background-color": c.buttonBg, border: `1px solid ${c.buttonBorder}`,108 "border-radius": "6px",109 cursor: fp.hasNextPage ? "pointer" : "not-allowed",110 transition: "all 0.2s",111 }}112 >113 Next114 </button>115 </div>116 </div>117 )}118 />119 );120}
TypeScriptFooterRendererDemo.ts
Copy
1import { SimpleTableVanilla } from "simple-table-core";2import type { Theme, FooterRendererProps } from "simple-table-core";3import { footerRendererConfig } from "./footer-renderer.demo-data";4import "simple-table-core/styles.css";56function getFooterColors(theme?: Theme) {7 switch (theme) {8 case "modern-dark":9 case "dark":10 return {11 background: "#1f2937",12 border: "#374151",13 text: "#d1d5db",14 buttonBg: "#374151",15 buttonBorder: "#4b5563",16 buttonActive: "#3b82f6",17 buttonText: "#d1d5db",18 buttonDisabled: "#6b7280",19 };20 case "light":21 case "modern-light":22 return {23 background: "white",24 border: "#f3f4f6",25 text: "#6b7280",26 buttonBg: "white",27 buttonBorder: "#e5e7eb",28 buttonActive: "#3b82f6",29 buttonText: "#374151",30 buttonDisabled: "#d1d5db",31 };32 default:33 return {34 background: "#f8fafc",35 border: "#e2e8f0",36 text: "#475569",37 buttonBg: "white",38 buttonBorder: "#e2e8f0",39 buttonActive: "#3b82f6",40 buttonText: "#64748b",41 buttonDisabled: "#cbd5e1",42 };43 }44}4546function createFooter(props: FooterRendererProps, theme?: Theme): HTMLElement {47 const c = getFooterColors(theme);4849 const wrapper = document.createElement("div");50 Object.assign(wrapper.style, {51 display: "flex",52 alignItems: "center",53 justifyContent: "space-between",54 padding: "16px 20px",55 backgroundColor: c.background,56 borderTop: `2px solid ${c.border}`,57 });5859 const info = document.createElement("span");60 Object.assign(info.style, { fontSize: "14px", fontWeight: "600", color: c.text });61 info.textContent = `Showing ${props.startRow}–${props.endRow} of ${props.totalRows} items`;62 wrapper.appendChild(info);6364 const controls = document.createElement("div");65 Object.assign(controls.style, { display: "flex", alignItems: "center", gap: "8px" });6667 function makeBtn(label: string, onClick: () => void, disabled: boolean, active = false) {68 const btn = document.createElement("button");69 btn.textContent = label;70 Object.assign(btn.style, {71 padding: "8px 16px",72 fontSize: "14px",73 fontWeight: "500",74 color: active ? "white" : disabled ? c.buttonDisabled : c.buttonActive,75 backgroundColor: active ? c.buttonActive : c.buttonBg,76 border: `1px solid ${c.buttonBorder}`,77 borderRadius: "6px",78 cursor: disabled ? "not-allowed" : "pointer",79 transition: "all 0.2s",80 minWidth: "40px",81 });82 btn.disabled = disabled;83 if (!disabled) btn.addEventListener("click", onClick);84 return btn;85 }8687 controls.appendChild(makeBtn("Previous", props.onPrevPage, !props.hasPrevPage));8889 const pages = document.createElement("div");90 Object.assign(pages.style, { display: "flex", gap: "4px" });91 for (let p = 1; p <= props.totalPages; p++) {92 const isActive = p === props.currentPage;93 const btn = makeBtn(String(p), () => props.onPageChange(p), false, isActive);94 btn.style.padding = "8px 12px";95 pages.appendChild(btn);96 }97 controls.appendChild(pages);9899 controls.appendChild(makeBtn("Next", () => props.onNextPage(), !props.hasNextPage));100101 wrapper.appendChild(controls);102 return wrapper;103}104105export function renderFooterRendererDemo(106 container: HTMLElement,107 options?: { height?: string | number; theme?: Theme }108): SimpleTableVanilla {109 const theme = options?.theme;110 const table = new SimpleTableVanilla(container, {111 columns: [...footerRendererConfig.headers],112 rows: footerRendererConfig.rows,113 height: options?.height ?? "400px",114 theme,115 enablePagination: true,116 rowsPerPage: 10,117 footerRenderer: (props) => createFooter(props, theme),118 hideFooter: false,119 });120 return table;121}122123124// footer-renderer.demo-data.ts125// Self-contained demo table setup for this example.126import type { ColumnDef, Row } from "simple-table-core";127128129export const footerRendererHeaders: ColumnDef[] = [130 { accessor: "id", label: "ID", width: 60, type: "number" },131 { accessor: "product", label: "Product Name", width: 220, type: "string" },132 { accessor: "category", label: "Category", width: 150, type: "string" },133 { accessor: "price", label: "Price", width: 100, type: "number" },134 { accessor: "stock", label: "Stock", width: 100, type: "number" },135 { accessor: "status", label: "Status", width: "1fr", type: "string" },136];137138export const footerRendererData: Row[] = [139 { id: 1, product: "MacBook Pro 16-inch M3 Max", category: "Laptops", price: 3499, stock: 28, status: "In Stock" },140 { id: 2, product: "Dell XPS 15 OLED Touchscreen", category: "Laptops", price: 2299, stock: 42, status: "In Stock" },141 { id: 3, product: "ThinkPad X1 Carbon Gen 11", category: "Laptops", price: 1899, stock: 35, status: "In Stock" },142 { id: 4, product: "HP Spectre x360 Convertible", category: "Laptops", price: 1649, stock: 51, status: "In Stock" },143 { id: 5, product: "ASUS ROG Strix Gaming Laptop", category: "Laptops", price: 2199, stock: 19, status: "In Stock" },144 { id: 6, product: "Logitech MX Master 3S Wireless", category: "Accessories", price: 99, stock: 342, status: "In Stock" },145 { id: 7, product: "Apple Magic Mouse Black", category: "Accessories", price: 89, stock: 218, status: "In Stock" },146 { id: 8, product: "Razer DeathAdder V3 Pro", category: "Accessories", price: 149, stock: 167, status: "In Stock" },147 { id: 9, product: "Microsoft Surface Precision Mouse", category: "Accessories", price: 79, stock: 203, status: "In Stock" },148 { id: 10, product: "Corsair K95 RGB Platinum XT", category: "Keyboards", price: 199, stock: 89, status: "In Stock" },149 { id: 11, product: "Keychron Q1 Pro Mechanical", category: "Keyboards", price: 189, stock: 134, status: "In Stock" },150 { id: 12, product: "Ducky One 3 TKL RGB", category: "Keyboards", price: 159, stock: 76, status: "In Stock" },151 { id: 13, product: "Leopold FC900R PD Cherry MX", category: "Keyboards", price: 169, stock: 54, status: "In Stock" },152 { id: 14, product: "LG UltraGear 27-inch 4K 144Hz", category: "Monitors", price: 799, stock: 31, status: "In Stock" },153 { id: 15, product: "Samsung Odyssey G9 Curved", category: "Monitors", price: 1299, stock: 18, status: "In Stock" },154 { id: 16, product: "Dell UltraSharp U2723DE 27in", category: "Monitors", price: 649, stock: 47, status: "In Stock" },155 { id: 17, product: "BenQ PD3220U Designer 32in", category: "Monitors", price: 1099, stock: 23, status: "In Stock" },156 { id: 18, product: "ASUS ProArt Display PA279CRV", category: "Monitors", price: 549, stock: 39, status: "In Stock" },157 { id: 19, product: "Sony WH-1000XM5 Noise Cancelling", category: "Audio", price: 399, stock: 127, status: "In Stock" },158 { id: 20, product: "Bose QuietComfort Ultra", category: "Audio", price: 429, stock: 98, status: "In Stock" },159 { id: 21, product: "Apple AirPods Max Space Gray", category: "Audio", price: 549, stock: 82, status: "In Stock" },160 { id: 22, product: "Sennheiser Momentum 4 Wireless", category: "Audio", price: 349, stock: 104, status: "In Stock" },161 { id: 23, product: "Logitech C920 HD Pro Webcam", category: "Video", price: 79, stock: 245, status: "In Stock" },162 { id: 24, product: "Elgato Facecam Pro 4K60", category: "Video", price: 299, stock: 67, status: "In Stock" },163 { id: 25, product: "Razer Kiyo Pro Ultra 4K", category: "Video", price: 329, stock: 41, status: "In Stock" },164 { id: 26, product: "Herman Miller Aeron Ergonomic", category: "Furniture", price: 1695, stock: 12, status: "Low Stock" },165 { id: 27, product: "Steelcase Leap V2 Office Chair", category: "Furniture", price: 1299, stock: 15, status: "In Stock" },166 { id: 28, product: "Autonomous SmartDesk Pro", category: "Furniture", price: 899, stock: 28, status: "In Stock" },167 { id: 29, product: "Uplift V2 Standing Desk Frame", category: "Furniture", price: 749, stock: 34, status: "In Stock" },168 { id: 30, product: "FlexiSpot E7 Plus Adjustable", category: "Furniture", price: 649, stock: 45, status: "In Stock" },169 { id: 31, product: "Anker PowerCore 20000mAh", category: "Power", price: 49, stock: 412, status: "In Stock" },170 { id: 32, product: "RAVPower 60W USB-C Charger", category: "Power", price: 39, stock: 387, status: "In Stock" },171 { id: 33, product: "Belkin BoostCharge Pro 3-in-1", category: "Power", price: 149, stock: 156, status: "In Stock" },172 { id: 34, product: "Samsung T7 Shield 2TB SSD", category: "Storage", price: 199, stock: 234, status: "In Stock" },173 { id: 35, product: "SanDisk Extreme Pro 1TB Portable", category: "Storage", price: 159, stock: 189, status: "In Stock" },174 { id: 36, product: "WD My Passport 5TB External", category: "Storage", price: 139, stock: 276, status: "In Stock" },175 { id: 37, product: "Crucial X9 Pro 4TB Rugged", category: "Storage", price: 289, stock: 143, status: "In Stock" },176 { id: 38, product: "CalDigit TS4 Thunderbolt 4 Dock", category: "Hubs & Docks", price: 399, stock: 52, status: "In Stock" },177 { id: 39, product: "Anker 577 Thunderbolt Docking", category: "Hubs & Docks", price: 299, stock: 78, status: "In Stock" },178 { id: 40, product: "HyperDrive Gen2 16-Port USB-C", category: "Hubs & Docks", price: 249, stock: 91, status: "In Stock" },179 { id: 41, product: "Blue Yeti X Professional USB", category: "Audio", price: 169, stock: 123, status: "In Stock" },180 { id: 42, product: "Shure MV7 Podcast Microphone", category: "Audio", price: 249, stock: 87, status: "In Stock" },181 { id: 43, product: "Elgato Wave:3 Premium USB", category: "Audio", price: 159, stock: 104, status: "In Stock" },182 { id: 44, product: "Rode NT-USB Mini Studio", category: "Audio", price: 99, stock: 167, status: "In Stock" },183 { id: 45, product: "Audio-Technica AT2020USB+", category: "Audio", price: 149, stock: 145, status: "In Stock" },184];185186export const footerRendererConfig = {187 headers: footerRendererHeaders,188 rows: footerRendererData,189 tableProps: {190 enablePagination: true,191 rowsPerPage: 10,192 },193} as const;194
Props
Footer Renderer Configuration
| Property | Required | Description | Example |
|---|---|---|---|
Property | Required | Description | Example |
footerRenderer | Optional | Custom footer that replaces the default. Receives pagination state and navigation handlers. | |
footerPosition"top" | "bottom" | Optional | Place the footer above ("top") or below ("bottom", default) the table body. | |
footerRenderKeystring | number | Optional | Cache key for custom footers that read external state. Change it when that state changes so the footer refreshes even if pagination inputs are unchanged. |
Renderer arguments
FooterRendererProps
| Property | Required | Description | Example |
|---|---|---|---|
Property | Required | Description | Example |
currentPagenumber | Required | The current page number (1-based index). | |
startRownumber | Required | The starting row number for the current page (1-based index). | |
endRownumber | Required | The ending row number for the current page (1-based index). | |
totalRowsnumber | Required | The total number of rows in the table. | |
totalPagesnumber | Required | The total number of pages based on rowsPerPage. | |
rowsPerPagenumber | Required | The number of rows displayed per page. | |
hasPrevPageboolean | Required | Boolean indicating if there is a previous page available. | |
hasNextPageboolean | Required | Boolean indicating if there is a next page available. | |
onPrevPage() => void | Required | Function to navigate to the previous page. | |
onNextPage() => Promise<void> | Required | Async function to navigate to the next page. | |
onPageChange(page: number) => void | Required | Function to navigate to a specific page number. | |
prevIconReactNode | Optional | Optional custom icon for the previous page button. | |
nextIconReactNode | Optional | Optional custom icon for the next page button. |