Documentation · @guiexpert/table 1.1.102

GuiExpert Table
documentation

Concepts, building blocks and reference for a data grid that displays millions of rows in any web framework. Read it once to understand how the table works – then use the training for guided examples and the API reference for every detail.

8frameworks, one core
0runtime dependencies
2 Mrows in the showcase
MITalso for commercial use
GET STARTED Your first table

Install the package for your framework and render a table in a few minutes.

Angular · React · Vue · Svelte · Preact · Web Component · JS
TRAINING Step by step

Short chapters with live demos – from the first table to row grouping, pivot tables, master/detail and formulas.

40 chapters · free
SHOWCASE Complete examples

2 million rows, heatmaps, tree tables, timetables and a live ticker – with source code.

runs in the browser
REFERENCE API reference

Every class, interface, option and method, generated from the source code.

TypeScript · with examples

What GuiExpert Table is

GuiExpert Table is a data grid for web applications that work with large tabular data sets: tens of thousands to millions of rows, dozens to hundreds of columns. It renders only the cells inside the visible area – vertically and horizontally – so the size of the DOM depends on the size of the viewport, not on the size of the data.

Of 100,000 rows only the about 15 rows inside the viewport are in the DOM; a spacer as high as all rows gives the scrollbar All rows (data) 100,000 objects in memory ⋮ 99,978 more The viewport (what you see) overflow: auto · fixed height row 50,001 row 50,002 row 50,003 row 50,004 row 50,005 row 50,006 row 50,007 row 50,008 translateY(first × rowHeight) Spacer height = rows × rowHeight → a real scrollbar On scroll first = scrollTop / rowHeight count = height / rowHeight ≈ 360 DOM elements for 1,000 or 1,000,000 rows
The DOM size depends on the height of the viewport – not on the number of rows.

The core, @guiexpert/table, is written in plain TypeScript and has no runtime dependencies. Thin wrappers make it available in Angular, React, Vue, Svelte, Solid and Preact and as a Web Component. Models, options, renderers and the API are identical in all of them.

  • Model-driven

    The table does not copy your data. While rendering it asks a model for each visible cell: value, row height, span, CSS classes, renderer.

  • Virtual in both directions

    Only visible rows and columns exist in the DOM. Fixed columns, header and footer stay in place while the body scrolls.

  • One core for every framework

    What you learn and build once – models, renderers, options – works unchanged in every supported framework.

  • Open by design

    Cells are plain DOM elements and colours are CSS variables. You decide what a cell contains and how the table looks.

Packages and frameworks

Every project needs the core. For a framework, add its wrapper – a small component that creates the table, forwards its events in the style of the framework and hands you the TableApi.

Eight thin wrappers on top of one framework-free core with zero runtime dependencies Your app Angular 14–22 React Vue 3 Svelte 4/5 Solid Preact Web Component Plain TS thin wrapper: creates the TableScope, maps events to props / outputs / CustomEvents @guiexpert/table – the core TableModel · TableOptions · TableScope · TableApi · renderers · CSS variables · 0 runtime dependencies → the same model and options work in every framework
FrameworkPackageComponentGuides
Plain TypeScript / JS @guiexpert/table new TableScope(…) Get started · chapter 5
Web Component @guiexpert/webcomponent-table <guiexpert-table> Get started · chapter 6
React 18 / 19 @guiexpert/react-table <GuiexpertTable> Get started · chapter 7
Vue 3 @guiexpert/vue3-table <GuiexpertTable> Get started · chapter 8
Angular 14 – 22 @guiexpert/angular-table <guiexpert-table> Get started · chapter 9
Angular 22+ (signals) @guiexpert/angular-table <guiexpert-table-signal> chapter 9
Svelte 4 / 5 @guiexpert/svelte-table <GuiexpertTable> Get started · chapter 10
Solid @guiexpert/solid-table <GuiexpertTable> chapter 10
Preact 10 @guiexpert/preact-table <GuiexpertTable> Get started · chapter 10

The wrappers declare their framework as a peer dependency. For Angular, the major version of the package follows the Angular major version (@guiexpert/angular-table@22 for Angular 22). For prototypes, @guiexpert/demo-table-models provides ready-made models (large data set, heatmap, tree and more).

Installation

Install the core and, if you use a framework, its wrapper:

Terminal window
npm install @guiexpert/table @guiexpert/react-table
# angular-table · vue3-table · svelte-table · solid-table · preact-table · webcomponent-table

Stylesheet

The core ships one stylesheet, css/main.css. It contains the colours as CSS variables (light and dark), the classes of the rendered cells and a few helper classes.

  • Bundler (Vite, webpack, esbuild): import "@guiexpert/table/css/main.css";
  • Plain HTML page: load it from a CDN – https://cdn.jsdelivr.net/npm/@guiexpert/table/css/main.css
  • Angular: nothing to do – the component contains the styles.
styles.css
.table-div {
height: 500px; /* or: height: calc(100vh - 120px); or flex: 1 in a flex column */
}

More on bundlers, CDN use and typical mistakes: chapter 4 (Installation).

The four building blocks

Every table consists of the same four parts – in plain TypeScript and in every framework: a host element, a table model (what to show), table options (how it behaves and looks) and a TableScope, which renders the table and returns the TableApi.

Host element, table model and options go into a TableScope, which renders the table and returns the TableApi 1 · Host element <div> with a fixed height 2 · TableModel what to show (rows, columns) 3 · TableOptions how it behaves and looks 4 · TableScope renders · scrolls · listens + DomService + EventListener getApi() TableApi scroll, sort, filter, copy, export, update cells …
The same four parts in every framework – the wrappers only hide step 4.

Without a framework you connect the four parts yourself:

main.ts
import {
EventAdapter, SimpleDomService, TableFactory, TableOptions, TableScope
} from "@guiexpert/table";
import "@guiexpert/table/css/main.css";
interface Order { id: number; customer: string; city: string; total: number; }
const orders: Order[] = [
{ id: 1, customer: "Miller Ltd.", city: "Berlin", total: 1250 },
{ id: 2, customer: "Acme Corp.", city: "Hamburg", total: 980 }
];
const tableModel = TableFactory.createTableModel({ // 2 · what to show
rows: orders,
properties: ["id", "customer", "city", "total"]
});
const tableScope = new TableScope(
document.querySelector<HTMLDivElement>("#orders")!, // 1 · host element with a height
tableModel,
new SimpleDomService(),
{ ...new TableOptions() }, // 3 · how it behaves and looks
new EventAdapter() // events – override what you need
);
tableScope.firstInit(); // 4 · render
const tableApi = tableScope.getApi(); // scroll, sort, filter, export …

A framework wrapper takes over step 4: you pass tableModel and tableOptions as inputs or props and receive the TableApi with the tableReady event (Angular 22+: <guiexpert-table-signal [(tableApi)]="api"> – the API as a signal). The guides for each framework are listed under Packages and frameworks.

The table model

A table model is not a data array. It is an object that answers the questions the table asks while it renders: how many rows and columns are there, what is the value of a cell, how high is a row, does a cell span several columns, which CSS classes does it get. Its interface is TableModelIf.

Header and footer need answers to most of these questions, too. That is why a table model consists of three area models (AreaModelIf) – one each for header, body and footer – plus the column definitions. Fixed columns split every area again into west, center and east:

Header, body and footer, each split into west (fixed left), center (scrolls) and east (fixed right) west (fixed) center (scrolls) east (fixed) header header-west header-center header-east body body-west body-center body-east footer footer-west footer-center footer-east fixedLeftColumnCount all other columns – virtual, horizontally and vertically fixedRightColumnCount scrolls
Every area asks its area model: getRowCount(), getValueAt(row, col), getRowHeight(row) …

What an area model answers

MethodQuestion
getRowCount()How many rows does the area have?
getValueAt()Which value is in row r, column c?
getRowHeight()How high is row r?
getConstantRowHeight()Are all rows equally high? Then scrolling to any position takes constant time.
getRowspanAt()Does the cell span several rows – or several columns (getColspanAt())?
getCustomClassesAt()Which CSS classes – or inline styles (getCustomStyleAt()) – does the cell get?
getCellRenderer()Which renderer draws the cell?
getTooltipAt()Which tooltip does the cell show?
isEditable()May the user edit the cell?

The table asks these questions only for cells that are visible. A model can therefore compute its values on demand – two million rows do not require two million objects in memory.

Ready-made area models

ClassUse it forTraining
AreaModelObjectArray an array of objects – what TableFactory creates from rows 12 · Array<T> → model
AreaModelArrayOfArrays rows as arrays of values (CSV, matrices) 14 · Array of arrays
AreaModelCellGroups grouped, collapsible header rows 15 · Header groups
AreaModelTree hierarchical rows for tree tables 30 · Tree table
AreaModelAggregateFooter a footer with totals of the filtered rows (footerAggregates) 16 · Footer totals
AreaModelRowGroups rows grouped by fields, with subtotals (RowGroupFactory) 36 · Row grouping
AreaModelPivot pivot rows with values per column group (PivotFactory) 37 · Pivot tables
AreaModelSheet a sparse spreadsheet body – only filled cells cost memory (SpreadsheetFactory) 40 · Spreadsheet
AreaModel base class for your own models: computed values, totals, server data 17 · Own AreaModel

Three ways to create a model

Choose the approach that matches your data. All three end in the same TableModelIf, and they can be combined – for example a body from an object array with a footer model of your own.

1 · From your data with TableFactory

TableFactory.createTableModel() covers most cases. Pass an array of objects with the properties to show – or with full column definitions (Column definitions):

const tableModel = TableFactory.createTableModel({
rows: orders,
properties: ["id", "customer", "city", "total"] // the columns, in this order
});

Data that already comes as rows of values – CSV, matrices, reports – goes in as an array of arrays:

const tableModel = TableFactory.createTableModel({
headerData: [["Name", "City", "Age"]], // one or more header rows
bodyData: [
["Anna", "Berlin", 34],
["Ben", "Hamburg", 28]
],
footerData: [["", "Average", "31"]],
columnSizes: [200, 150, 80]
});

All parameters: CreateTableModelPara.

2 · Assemble your own area models

Implement an area model when the data is computed, very large or loaded on demand – and pass it to the factory as bodyAreaModel, headerAreaModel or footerAreaModel. The base class AreaModel provides sensible defaults, so only a few methods are needed:

million-rows-model.ts
import { AreaModel, ColumnDef, TableFactory, px120, px200 } from "@guiexpert/table";
class MillionRowsModel extends AreaModel {
constructor(private readonly rowCount: number) { super(); }
override getRowCount(): number { return this.rowCount; }
override getRowHeight(_rowIndex: number): number { return 34; }
override getConstantRowHeight(): number | undefined { return 34; } // constant-time scrolling
override getValueAt(rowIndex: number, columnIndex: number): string | number {
return columnIndex === 0 ? "id" + rowIndex : rowIndex * 3; // computed on demand
}
}
const tableModel = TableFactory.createTableModel({
bodyAreaModel: new MillionRowsModel(2_000_000),
columnDefs: [new ColumnDef("id", "ID", px120), new ColumnDef("value", "Value", px200)]
});

The same pattern gives you a footer with totals of the filtered rows (chapter 16 (Footer totals)) or a model that loads pages from a server (chapter 17 (Own AreaModel)).

3 · Extend an existing model

To change a single aspect, derive from a ready-made model and override one method. Here negative balances get the error colour:

balance-model.ts
import { AreaModelArrayOfArrays, TableFactory } from "@guiexpert/table";
class BalanceModel extends AreaModelArrayOfArrays<string | number> {
override getCustomClassesAt(rowIndex: number, columnIndex: number): string[] {
const value = this.getValueAt(rowIndex, columnIndex);
return typeof value === "number" && value < 0 ? ["ge-table-color-error"] : [];
}
}
const tableModel = TableFactory.createTableModel({
headerData: [["Account", "Balance"]],
bodyAreaModel: new BalanceModel("body", [["Cash", 1200], ["Loan", -350]], 34),
columnSizes: [200, 120]
});

Spans, tooltips, renderers and editability work the same way: override getRowspanAt(), getTooltipAt(), getCellRenderer() or isEditable().

Column definitions

A ColumnDef describes one column: which property it shows, its header label and width, alignment, renderers, sorting and editing. Use the constructor for the short form and ColumnDef.create() for named parameters:

import { ColumnDef, ColumnDefIf, TableFactory, TrueFn, px80, px120, px200 } from "@guiexpert/table";
const columnDefs: ColumnDefIf[] = [
new ColumnDef("id", "ID", px80), // property, header label, width
ColumnDef.create({ // named parameters
property: "customer",
headerLabel: "Customer",
width: px200,
sortable: TrueFn
}),
ColumnDef.create({
property: "total",
headerLabel: "Total",
width: px120,
headerClasses: ["ge-table-text-align-right"],
bodyClasses: ["ge-table-text-align-right"],
sortable: TrueFn,
sortComparator: (a: number, b: number) => a - b
})
];
const tableModel = TableFactory.createTableModel({
rows: orders,
columnDefs,
fixedLeftColumnCount: 1 // "ID" stays visible while scrolling
});
FieldPurpose
property, headerLabelthe value shown in the body (dot notation for nested objects, e.g. address.city) and the header text
width, minWidth, maxWidthsizes as Size – constants px0 … px500
headerClasses, bodyClasses, footerClassesCSS classes per area, e.g. the alignment helpers ge-table-text-align-left | center | right
headerRenderer, bodyRenderer, footerRenderera CellRendererIf per area (chapter 25 (Cell renderers))
sortable, sortComparator, sortStatesorting by header double-click, own comparators, initial sort state
editable, getEditRenderer, editInputPipeinline editing, the editor and the conversion of the input
isVisiblehide a column (width 0) – it is skipped by clipboard and Excel export

Fixed columns are part of the model, not of the column: fixedLeftColumnCount and fixedRightColumnCount in CreateTableModelPara (chapter 18 (Fixed columns)).

Table options

TableOptions holds the behaviour and look of a table. Start from the defaults and change only what you need:

import { TableOptions, TableOptionsIf } from "@guiexpert/table";
const tableOptions: TableOptionsIf = {
...new TableOptions(), // all defaults …
hoverColumnVisible: false, // … and only what you change
columnsResizable: true,
columnsDraggable: true,
defaultRowHeights: { header: 34, body: 34, footer: 0 }
};
TopicOptions
Layout defaultRowHeights overflowX overflowY resizeEventDebounceDelay
Mouse hoverRowVisible hoverColumnVisible columnsResizable columnsDraggable columnResizeHandleWidthInPx
Borders verticalBorderVisible horizontalBorderVisible headerSeparatorBorderVisible footerSeparatorBorderVisible fixedWestSeparatorBorderVisible fixedEastSeparatorBorderVisible tableTopBorderVisible tableBottomBorderVisible
Sorting and filtering sortedOptions sortOrder externalFilterFunction
Selection and keyboard getSelectionModel getFocusModel shortcutActionIdMapping shortcutActionsDisabled
Trees, groups, editing treeOptions headerGroupOptions showCheckboxWihoutExtraColumn getEditRenderer
State autoRestoreOptions initialState
Accessibility and overlays ariaLabel ariaLabelledBy ariaTexts overlayOptions
Editing and rows fillHandle batchEdit rowDragOptions masterDetail
Direction direction
Spreadsheet editing clipboard undo cellFormulas spreadsheet

Every option with its default value: TableOptionsIf. Walkthrough: chapter 19 (TableOptions).

TableApi and events

After the first render you receive a TableApi – from tableScope.getApi() or the tableReady event of a wrapper. It is the interface for everything you do with an existing table:

TaskMethods
Scrolling scrollToIndex() scrollToPixel() ensureRowIsVisible() getFirstVisibleRowIndex() getDisplayedRowCount()
Sorting and filtering sortByColumn() sort() reSort() externalFilterChanged()
Rows (object models) setRows() addRows() addRowsAt() updateRows() removeRows() findRows()
Cells and rendering updateCells() repaint() repaintHard() getTableModel() getBodyModel()
Columns autoResizeColumns() setColumnWidth() recalcColumnWidths() setColumnVisible() setColumnFormula() setShowValuesAs()
Grouping and pivot setRowGroupBy() setPivotConfig() getSelectionAggregates()
Editing, rows and details applyCellChanges() commitBatchEdit() cancelBatchEdit() moveRows() setDetailExpanded()
State, overlays, announcements getState() applyState() resetState() showLoadingOverlay() hideOverlay() announce()
Selection and keyboard getSelectionModel() setSelectionModel() clearSelection() copyToClipboard() triggerAction()
Spreadsheet undo() redo() pasteText() setCellInput() getCellInput() insertRows() deleteRows() insertColumns() deleteColumns() goToAddress() subscribe()
Export and lifecycle downloadExcel() destroy()

Data flow: filter, sort, display

Filtering and sorting do not change your data. The body model keeps all rows and derives the displayed rows from them; footer totals, clipboard and Excel export work on exactly these displayed rows.

All rows pass the external filter and the sort order; the result feeds the body, the footer totals, copy and the Excel export of the view all rows getAllRows() filter externalFilterFunction sort comparator / sortByColumn displayed rows getFilteredRows() body (visible part) footer totals copy · Excel view externalFilterChanged() runs the filter again and keeps the last sort order. ExcelService with getAllRows() ignores filter and sorting.

Events

In plain TypeScript, pass an EventListenerIf to the TableScope – usually a subclass of EventAdapter that overrides only the methods you need. The wrappers forward the same events as outputs, props or CustomEvents:

import { EventAdapter, GeMouseEvent, SelectionModelIf } from "@guiexpert/table";
class OrderTableEvents extends EventAdapter {
override onMouseClicked(evt: GeMouseEvent): void {
if (evt.areaIdent === "body" && evt.clickCount === 2) {
console.info("open the order in row", evt.rowIndex);
}
}
override onSelectionChanged(model: SelectionModelIf): void {
console.info("selected ranges", model.getRanges());
}
}
Wrapper eventEventListenerIfPayloadNote
tableReady – TableApi once, after the first render (wrappers only; <guiexpert-table-signal>: [(tableApi)] signal instead)
mouseClicked onMouseClicked() GeMouseEvent area, row, column, click count; debounced in Angular and Vue
mouseMoved onMouseMoved() GeMouseEvent
contextmenu onContextmenu() GeMouseEvent for your own context menu; <guiexpert-table-signal>: contextMenu
mouseDragging onMouseDragging() GeMouseEvent
mouseDraggingEnd onMouseDraggingEnd() GeMouseEvent Angular: mouseDraggingEnded
checkboxChanged onCheckboxChanged() checked rows row and tree checkboxes
modelChanged onModelChanged() GeModelChangeEvent after editing, filling, a batch commit or applyCellChanges – one event with all changes
selectionChanged onSelectionChanged() SelectionModelIf
focusChanged onFocusChanged() FocusModelIf
scroll onScroll() GeScrollEvent scrollLeft from the start of the row (also in right-to-left)
stateChanged onStateChanged() GeStateChangeEvent layout, sorting, selection … changed (debounced)
rowDragEnd onRowDragEnd() GeRowDragEvent also rowDragStart, rowDragMove
batchEditChanged onBatchEditChanged() GeBatchEditEvent start, change, commit, cancel
detailExpandedChanged onDetailExpandedChanged() GeDetailEvent a detail row opened or closed
editingChanged onEditingChanged() GeEditEvent the cell editor: start, input, commit, cancel
undoStackChanged onUndoStackChanged() GeUndoEvent canUndo, canRedo and the labels – for undo buttons

Features at a glance

Each feature is built from the concepts above – model, column definitions, options, API and events. The training covers each one in a chapter of its own, with a live demo.

FeatureHowTraining
Millions of rows an own area model that computes cells on demand – AreaModel, getConstantRowHeight() chapter 17
Fixed columns left and right, set in the model – fixedLeftColumnCount, fixedRightColumnCount chapter 18
Header groups multi-row, collapsible headers – AreaModelCellGroups, buildColumnDefs() chapter 15
Footer totals one option: sum, average, count … of the filtered rows, plus sums of the selection – footerAggregates, getSelectionAggregates() chapter 16
Row numbers a fixed “#” column, one flag – rowNumbersVisible chapter 18
Row grouping groups with subtotals, regroup at runtime – createTableModel(), setRowGroupBy() chapter 36
Pivot tables row fields × column fields, subtotals and totals – createTableModel(), setPivotConfig() chapter 37
Calculated columns and formulas valueGetter, “show values as”, formulas without eval – valueGetter, formula chapter 39
Column resizing and reordering drag handles and drag & drop – columnsResizable, columnsDraggable, autoResizeColumns() chapter 19
Sorting header double-click, own comparators, from code – sortComparator, sortByColumn() chapter 20
Filtering your filter UI – the table asks per row – externalFilterFunction, externalFilterChanged() chapter 21
Selection and clipboard cells, rows, columns or ranges; copy, cut and paste with Excel – SelectionModel, clipboard, pasteText() chapter 22
Events and context menu override only the events you need – EventAdapter, onContextmenu() chapter 23
Excel export real .xlsx files without a library – downloadExcel(), ExcelService chapter 24
Cell renderers built-in renderers or one method of your own – CellRendererIf, StarRatingCellRenderer chapter 25
Framework components in cells React, Vue, Angular, Svelte … per visible cell – CellRendererIf chapter 26
Heatmaps and conditional formatting style and classes per cell, colour gradients – getCustomStyleAt(), GeCssColorUtil chapter 27
Theming and dark mode CSS variables, light and dark built in – CssVars chapter 28
Rowspan and colspan merged cells, defined by the model – getRowspanAt(), getColspanAt() chapter 29
Tree table with checkboxes hierarchical data, expanded or collapsed – buildTreeRows(), onCheckboxChanged() chapter 30
Inline editing Excel-style: type to overwrite, F2, Enter/Tab; undo and redo – editing, undo, editable chapter 31
Fill handle and batch edit continue series, collect changes, then save – fillHandle, batchEdit chapter 31
Row drag drag rows into order – mouse, touch, keyboard – rowDrag, moveRows() chapter 23
Master/detail expandable detail rows with any content – masterDetail chapter 38
Accessibility WAI-ARIA grid, keyboard, screen reader announcements – ariaLabel, announce() chapter 35
Loading and empty overlays CSS spinner, “No rows”, “No matching rows” – showLoadingOverlay(), overlayOptions chapter 32
Right-to-left Arabic, Hebrew, Persian – one option – direction chapter 28
Spreadsheet A1 cell formulas, copy & paste with Excel, undo, formula bar – one call – create(), cellFormulas, FormulaBar chapter 40
Live data update single cells – only visible ones are redrawn – updateCells() chapter 32
Persisted state column layout, sorting, scroll position, tree, checked and selected rows – automatically or as JSON for your server – autoRestoreOptions, getState() chapter 33

Styling and theming

All colours of the table are CSS variables with the prefix --ge-table-. The classes of the rendered elements only reference these variables, so a theme is a set of variable values – no selectors to override, no build step. main.css contains a light and a dark set; the table follows the data-theme attribute of the <html> element.

Your own theme

Override the variables in a stylesheet that is loaded after main.css. CSS variables are inherited: put them on html to theme every table, or on a class around one table to theme only that one.

theme.css
/* light theme (default) */
.brand-table {
--ge-table-header-center-bg: #1e293b;
--ge-table-header-center-text: #f8fafc;
--ge-table-body-center-text: #0f172a; /* the body text follows the header text by default */
--ge-table-row-even-bg: #f8fafc; /* zebra rows */
--ge-table-hover-row-bg: rgba(54, 187, 108, 0.12);
--ge-table-body-center-selected-range-bg: rgba(54, 187, 108, 0.25);
}
/* dark theme */
html[data-theme="dark"] .brand-table {
--ge-table-header-center-bg: #0b1220;
--ge-table-body-center-text: #e2e8f0;
--ge-table-row-even-bg: rgba(148, 163, 184, 0.06);
}

Light, dark and the system setting

Most applications offer light, dark and “follow the system”. Set data-theme in an inline script before the first paint, so that the page does not flash in the wrong theme:

index.html
<script>
const root = document.documentElement;
const system = matchMedia("(prefers-color-scheme: dark)");
const systemTheme = () => (system.matches ? "dark" : "light");
// saved choice ("light" | "dark") or, without one, the system setting
root.dataset.theme = localStorage.getItem("theme") ?? systemTheme();
system.addEventListener("change", () => {
if (!localStorage.getItem("theme")) root.dataset.theme = systemTheme();
});
// e.g. from a toggle button
function setTheme(theme) {
root.dataset.theme = theme;
localStorage.setItem("theme", theme);
}
</script>

The table switches with the attribute – no API call needed. Live example: chapter 28 (Theming & dark mode); several complete themes: showcase Look and feel.

CSS variables

124 variables, grouped by what they colour. Header, body and footer each have their own set per side (west, center, east); west and east default to the center values.

All 124 variables with their light and dark defaults

Table

VariableLightDark
--ge-table-bg #fff #111827
--ge-table-border #ccc oklch(32% 0 0)

Selected cells

VariableLightDark
--ge-table-body-west-selected-range-bg rgba(0, 152, 219, 0.4) oklch(19% 0.2 205.2)
--ge-table-body-center-selected-range-bg rgba(0, 152, 219, 0.4) oklch(19% 0.2 205.2)
--ge-table-body-east-selected-range-bg rgba(0, 152, 219, 0.4) oklch(19% 0.2 205.2)
--ge-table-body-west-selected-range-text #000 #0098db
--ge-table-body-center-selected-range-text #000 #0098db
--ge-table-body-east-selected-range-text #000 #0098db

Header

VariableLightDark
--ge-table-header-west-bg var(--ge-table-header-center-bg) #111827
--ge-table-header-center-bg rgba(233, 233, 233, 0.5) #111827
--ge-table-header-east-bg var(--ge-table-header-center-bg) #111827
--ge-table-header-west-text var(--ge-table-header-center-text) oklch(100% 0 0)
--ge-table-header-center-text #000 oklch(100% 0 0)
--ge-table-header-east-text var(--ge-table-header-center-text) oklch(100% 0 0)
--ge-table-header-west-horizontal-border var(--ge-table-header-center-horizontal-border) #313641
--ge-table-header-west-vertical-border var(--ge-table-header-center-vertical-border) oklch(32% 0 0)
--ge-table-header-center-horizontal-border #ddd #313641
--ge-table-header-center-vertical-border #ccc oklch(32% 0 0)
--ge-table-header-east-horizontal-border var(--ge-table-header-center-horizontal-border) #313641
--ge-table-header-east-vertical-border var(--ge-table-header-center-vertical-border) oklch(32% 0 0)

Body

VariableLightDark
--ge-table-body-west-bg var(--ge-table-header-center-bg) #23272e
--ge-table-body-center-bg rgba(255,255,255, 0.5) #23272e55
--ge-table-body-east-bg var(--ge-table-header-center-bg) #23272e
--ge-table-body-west-text var(--ge-table-header-center-text) oklch(100% 0 0)
--ge-table-body-center-text var(--ge-table-header-center-text) oklch(100% 0 0)
--ge-table-body-east-text var(--ge-table-header-center-text) oklch(100% 0 0)
--ge-table-body-west-horizontal-border var(--ge-table-header-west-horizontal-border) #313641
--ge-table-body-west-vertical-border var(--ge-table-header-west-vertical-border) oklch(32% 0 0)
--ge-table-body-center-horizontal-border #bbb #313641
--ge-table-body-center-vertical-border #ddd oklch(32% 0 0)
--ge-table-body-east-horizontal-border var(--ge-table-header-east-horizontal-border) #313641
--ge-table-body-east-vertical-border var(--ge-table-header-east-vertical-border) oklch(32% 0 0)

Footer

VariableLightDark
--ge-table-footer-west-bg var(--ge-table-header-center-bg) #111827
--ge-table-footer-center-bg var(--ge-table-header-center-bg) #111827
--ge-table-footer-east-bg var(--ge-table-header-center-bg) #111827
--ge-table-footer-west-text var(--ge-table-header-center-text) var(--ge-table-header-center-text)
--ge-table-footer-center-text var(--ge-table-header-center-text) var(--ge-table-header-center-text)
--ge-table-footer-east-text var(--ge-table-header-center-text) var(--ge-table-header-center-text)
--ge-table-footer-west-horizontal-border var(--ge-table-header-west-horizontal-border) oklch(30% 0 0)
--ge-table-footer-west-vertical-border var(--ge-table-header-west-vertical-border) oklch(32% 0 0)
--ge-table-footer-center-horizontal-border var(--ge-table-header-center-horizontal-border) oklch(30% 0 0)
--ge-table-footer-center-vertical-border var(--ge-table-header-center-vertical-border) oklch(32% 0 0)
--ge-table-footer-east-horizontal-border var(--ge-table-header-east-horizontal-border) oklch(30% 0 0)
--ge-table-footer-east-vertical-border var(--ge-table-header-east-vertical-border) oklch(32% 0 0)

Zebra rows and columns

VariableLightDark
--ge-table-row-odd-bg transparent #11151F55
--ge-table-row-even-bg transparent #1A202F55
--ge-table-column-odd-bg transparent transparent
--ge-table-column-even-bg transparent transparent

Hover and focus

VariableLightDark
--ge-table-hover-column-bg rgba(0, 224, 255, 0.27) rgba(0, 224, 255, 0.27)
--ge-table-hover-row-bg rgba(0, 224, 255, 0.27) rgba(0, 224, 255, 0.27)
--ge-table-focus-border rgb(0, 0, 255) rgb(0, 255, 255)
--ge-table-detail-toggle-hover-bg rgba(128, 128, 128, 0.15) rgba(255, 255, 255, 0.12)

Resizing and dragging columns

VariableLightDark
--ge-table-column-resize-handle-border rgb(0, 255, 255) rgb(0, 255, 255)
--ge-table-dragged-col-div-bg lightcyan oklch(0% 0 0)
--ge-table-drop-zone-bg rgba(244, 255, 242, 0.6) oklch(0% 0 0)
--ge-table-color-col-resize-handle-hover-bg cyan cyan
--ge-table-color-dragged-col-div-bg #fafafa #fafafa
--ge-table-color-drop-zone-bg #fafafa #fafafa
--ge-table-color-drop-zone #fafafa #fafafa
--ge-table-row-drag-handle-color #6b7280 #9ca3af
--ge-table-row-drop-indicator-color #1a73e8 #8ab4f8
--ge-table-row-drop-inside-bg rgba(26, 115, 232, 0.12) rgba(138, 180, 248, 0.16)
--ge-table-row-drag-ghost-bg #fff #1f2937
--ge-table-row-drag-ghost-text #1f2937 #f3f4f6
--ge-table-row-drag-ghost-border #1a73e8 #8ab4f8
--ge-table-row-dragging-opacity 0.4 0.4

Tree, errors and built-in renderers

VariableLightDark
--ge-table-color-error-text #e00034 #ffa8b5
--ge-table-tree-arrow-collapsed-color #e00034 #ffa8b5
--ge-table-color-tree-arrow-collapsed-color var(--ge-table-body-west-text) var(--ge-table-body-west-text)
--ge-table-color-star-rating #a86f00 #fcb515
--ge-table-color-progress-bar #1e874b #36BB6C
--ge-table-row-group-bg rgba(0, 0, 0, 0.045) rgba(255, 255, 255, 0.06)
--ge-table-row-group-text inherit inherit
--ge-table-row-group-label-font-weight 600 600
--ge-table-pivot-subtotal-bg rgba(0, 0, 0, 0.035) rgba(255, 255, 255, 0.05)
--ge-table-pivot-subtotal-font-weight 600 600
--ge-table-pivot-total-bg rgba(0, 0, 0, 0.07) rgba(255, 255, 255, 0.1)
--ge-table-pivot-total-font-weight 600 600
--ge-table-pivot-empty-text #6b7280 #9ca3af
--ge-table-cell-error-color #b91c1c #f87171
--ge-table-overlay-bg rgba(255, 255, 255, 0.7) rgba(17, 24, 39, 0.7)
--ge-table-overlay-content-bg #fff #1f2937
--ge-table-overlay-color #333 #ddd
--ge-table-spinner-color #1a73e8 #8ab4f8
--ge-table-spinner-track-color rgba(0, 0, 0, 0.12) rgba(255, 255, 255, 0.18)
--ge-table-spinner-size 28px 28px
--ge-table-row-number-bg var(--ge-table-header-west-bg) var(--ge-table-header-west-bg)
--ge-table-row-number-color #555 #aaa
--ge-table-fill-handle-color #0a6ebd #8ab4f8
--ge-table-fill-handle-border #fff #1e1e1e
--ge-table-fill-preview-border #0a6ebd #8ab4f8
--ge-table-cell-pending-bg #fff3c4 #4a3b0b
--ge-table-cell-pending-text #3d2e00 #fde68a
--ge-table-cell-pending-marker #c27c00 #f5b82e
--ge-table-cell-invalid-bg #fde2e1 #5c1d1a
--ge-table-cell-invalid-text #8a1c14 #fecaca
--ge-table-cell-invalid-border #d93025 #f28b82
--ge-table-detail-bg #f4f6f9 #1b1f24
--ge-table-detail-border #c9d1db #3c434d
--ge-table-detail-toggle-color #5f6368 #bdc1c6
--ge-table-cell-editor-bg #fff #1f2937
--ge-table-cell-editor-text #111 #f3f4f6
--ge-table-cell-editor-border #0a6ebd #8ab4f8
--ge-table-cell-editor-shadow 0 2px 6px rgba(0, 0, 0, 0.2) 0 2px 6px rgba(0, 0, 0, 0.6)
--ge-table-cell-editor-invalid-border #d93025 #f28b82
--ge-table-cut-range-border #0a6ebd #8ab4f8
--ge-table-cut-range-border-alt #9ec9ec #3c5a85
--ge-table-formula-ref-0 #1a73e8 #8ab4f8
--ge-table-formula-ref-1 #d93025 #f28b82
--ge-table-formula-ref-2 #7b1fa2 #d7aefb
--ge-table-formula-ref-3 #188038 #81c995
--ge-table-formula-ref-4 #c26401 #fcad70
--ge-table-formula-ref-5 #00838f #78d9ec
--ge-table-formula-ref-6 #c2185b #ff8bcb
--ge-table-formula-ref-7 #5d4037 #d7ccc8
--ge-table-formula-popup-bg #fff #1f2937
--ge-table-formula-popup-text #111 #f3f4f6
--ge-table-formula-popup-muted #5f6368 #9aa0a6
--ge-table-formula-popup-border #c4c7c5 #4b5563
--ge-table-formula-popup-active-bg #e8f0fe #374151
--ge-table-formula-bar-bg #fff #1f2937
--ge-table-formula-bar-text #111 #f3f4f6
--ge-table-formula-bar-muted #5f6368 #9aa0a6
--ge-table-formula-bar-border #c4c7c5 #4b5563

Source: table-color-vars.css. From TypeScript: CssVars.

CSS classes

The rendered elements carry stable class names. Use them for layout details that are not covered by a variable – and in tests. Every cell also has the attributes data-area, data-row-index and data-col-index.

ClassElement
ge-tablethe host element
ge-table-<area>-<side>the nine areas, e.g. ge-table-body-center – background and text colour
ge-table-col-divevery cell, plus ge-table-col-div-<column index>
ge-table-label-div, ge-table-labelthe label inside a cell (padding, ellipsis)
ge-table-text-align-left | center | righthelper classes for headerClasses, bodyClasses, footerClasses
ge-table-row-even | oddrows of the scrolling body (zebra) – ge-table-column-even | odd for columns
ge-table-<area>-<side>-horizontal-bordergrid lines – likewise …-vertical-border
ge-table-hover-row, ge-table-hover-columnhover highlight
ge-table-body-<side>-selected-rangeselected cells
ge-table-focus-borderthe border of the focused cell
ge-table-sorted-icon-divthe sort icon in the header
ge-table-column-resize-handle, ge-table-drop-zoneresizing and reordering columns
ge-table-cell-editor-inputthe inline editor
ge-table-tree-cell, ge-table-tree-deep-0 … 5tree cells and their indentation per level
ge-table-row-checkbox-divthe row checkbox
ge-table-color-errorerror colour for text, e.g. negative values
ge-table-progress-bar, ge-star-rating-divbuilt-in renderers

Performance checklist

The table is fast by default. These points keep it fast with very large data and frequent updates:

  • Constant row height. If all rows have the same height, return it from getConstantRowHeight() in your own models. The table then finds the first visible row by a division instead of a binary search over the row heights (which it uses for different heights since 1.1.97). The built-in models do this automatically.
  • Stable model and options. Create them once; a new object rebuilds the table (see The four building blocks).
  • Filter, don't rebuild. Use externalFilterFunction and externalFilterChanged() instead of a new model with fewer rows.
  • Update cells, not tables. For changing values use updateCells() – only the changed cells that are visible are redrawn.
  • Lazy values. getValueAt() and renderers are called for visible cells only. Compute expensive values there instead of preparing them for all rows.
  • Clean up. Renderers that attach listeners or framework components return a cleanup function. Call destroy() before you remove a table you created with TableScope; the wrappers do it for you.

Measure it yourself: showcases Large dataset (2 million rows) and Render speed.

What is not built in

GuiExpert Table concentrates on rendering, models and interaction. Some things are deliberately left to the application, others are not available yet. The usual approach for each:

TopicApproach
Filter UIyour own inputs – the table provides externalFilterFunction (chapter 21 (Filtering))
Context menuthe contextmenu event plus a menu in the style of your app (chapter 23 (Events & context menu))
Paginationnot needed for display thanks to virtual scrolling; to load data page by page, use an own area model (chapter 17 (Own AreaModel))
Server-side row modelan own AreaModel that requests the visible rows from your backend
Show / hide columns at runtimesetColumnVisible() (also setHeaderVisible() / setFooterVisible())
Group panel, pivot field listyour UI around setRowGroupBy() and setPivotConfig()
Toolbar for undo, insert rows …your buttons calling undo(), insertRows() – paste, undo/redo and the formula editor are built in since 1.1.99 (chapter 40 (Spreadsheet))

A checklist for planning your own table – and for deciding when another product fits better: chapter 34 (Checklist & finale).

Help and feedback

GuiExpert Table is MIT-licensed and free to use, also in commercial applications – details on the licence page.