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.
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.
| Framework | Package | Component | Guides |
|---|---|---|---|
| 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:
npm install @guiexpert/table @guiexpert/react-table# angular-table · vue3-table · svelte-table · solid-table · preact-table · webcomponent-tableStylesheet
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.
.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.
Without a framework you connect the four parts yourself:
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 · renderconst 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:
getRowCount(), getValueAt(row, col), getRowHeight(row) …What an area model answers
| Method | Question |
|---|---|
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
| Class | Use it for | Training |
|---|---|---|
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:
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:
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});| Field | Purpose |
|---|---|
property, headerLabel | the value shown in the body (dot notation for nested objects, e.g. address.city) and the header text |
width, minWidth, maxWidth | sizes as Size – constants px0 … px500 |
headerClasses, bodyClasses, footerClasses | CSS classes per area, e.g. the alignment helpers ge-table-text-align-left | center | right |
headerRenderer, bodyRenderer, footerRenderer | a CellRendererIf per area (chapter 25 (Cell renderers)) |
sortable, sortComparator, sortState | sorting by header double-click, own comparators, initial sort state |
editable, getEditRenderer, editInputPipe | inline editing, the editor and the conversion of the input |
isVisible | hide 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 }};| Topic | Options |
|---|---|
| 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:
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.
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 event | EventListenerIf | Payload | Note |
|---|---|---|---|
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.
| Feature | How | Training |
|---|---|---|
| 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.
/* 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:
<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
| Variable | Light | Dark |
|---|---|---|
--ge-table-bg |
#fff
|
#111827
|
--ge-table-border |
#ccc
|
oklch(32% 0 0)
|
Selected cells
| Variable | Light | Dark |
|---|---|---|
--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
| Variable | Light | Dark |
|---|---|---|
--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
| Variable | Light | Dark |
|---|---|---|
--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
| Variable | Light | Dark |
|---|---|---|
--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
| Variable | Light | Dark |
|---|---|---|
--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
| Variable | Light | Dark |
|---|---|---|
--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
| Variable | Light | Dark |
|---|---|---|
--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
| Variable | Light | Dark |
|---|---|---|
--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.
| Class | Element |
|---|---|
ge-table | the host element |
ge-table-<area>-<side> | the nine areas, e.g. ge-table-body-center – background and text colour |
ge-table-col-div | every cell, plus ge-table-col-div-<column index> |
ge-table-label-div, ge-table-label | the label inside a cell (padding, ellipsis) |
ge-table-text-align-left | center | right | helper classes for headerClasses, bodyClasses, footerClasses |
ge-table-row-even | odd | rows of the scrolling body (zebra) – ge-table-column-even | odd for columns |
ge-table-<area>-<side>-horizontal-border | grid lines – likewise …-vertical-border |
ge-table-hover-row, ge-table-hover-column | hover highlight |
ge-table-body-<side>-selected-range | selected cells |
ge-table-focus-border | the border of the focused cell |
ge-table-sorted-icon-div | the sort icon in the header |
ge-table-column-resize-handle, ge-table-drop-zone | resizing and reordering columns |
ge-table-cell-editor-input | the inline editor |
ge-table-tree-cell, ge-table-tree-deep-0 … 5 | tree cells and their indentation per level |
ge-table-row-checkbox-div | the row checkbox |
ge-table-color-error | error colour for text, e.g. negative values |
ge-table-progress-bar, ge-star-rating-div | built-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
externalFilterFunctionandexternalFilterChanged()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 withTableScope; 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:
| Topic | Approach |
|---|---|
| Filter UI | your own inputs – the table provides externalFilterFunction (chapter 21 (Filtering)) |
| Context menu | the contextmenu event plus a menu in the style of your app (chapter 23 (Events & context menu)) |
| Pagination | not 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 model | an own AreaModel that requests the visible rows from your backend |
| Show / hide columns at runtime | setColumnVisible() (also setHeaderVisible() / setFooterVisible()) |
| Group panel, pivot field list | your 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
Ask with the tag guiexpert-table – answers stay findable for others.
Core, all wrappers, demos and this website in one repository. Bug reports and ideas are welcome.
PLAYGROUND StackBlitzSmall examples to fork and try in the browser – no local setup.
FEEDBACK feedback@gui.expertMissing a feature, a framework or an example? Let us know.
GuiExpert Table is MIT-licensed and free to use, also in commercial applications – details on the licence page.