Core Features
This page documents built-in grid features that don’t require plugins — the interactive playground, column configuration, data formatting, styling, shell layout, loading states, events, methods, and more.
Basic Usage
Section titled “Basic Usage”Interactive Playground
Section titled “Interactive Playground”Experiment with the grid’s core options in real time. Adjust the row count, toggle columns, change the fit mode, and enable or disable sortable/resizable columns.
<tbw-grid style="height: 420px;"></tbw-grid> import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/editing';
type ColumnKey = 'id' | 'name' | 'active' | 'score' | 'created' | 'role';
const grid = queryGrid('tbw-grid');
if (container && grid) { const allColumnDefs: Record<ColumnKey, ColumnConfig> = { id: { field: 'id', header: 'ID', type: 'number', sortable: true, resizable: true }, name: { field: 'name', header: 'Name', sortable: true, resizable: true }, active: { field: 'active', header: 'Active', type: 'boolean', sortable: true }, score: { field: 'score', header: 'Score', type: 'number', sortable: true, resizable: true }, created: { field: 'created', header: 'Created', type: 'date', sortable: true, resizable: true, }, role: { field: 'role', header: 'Role', type: 'select', sortable: true, options: [ { label: 'Admin', value: 'admin' }, { label: 'User', value: 'user' }, { label: 'Guest', value: 'guest' }, ], }, };
function generateRows(count: number) { const roles = ['admin', 'user', 'guest']; const names = ['Alice', 'Bob', 'Carol', 'Dan', 'Eve', 'Frank', 'Grace', 'Henry']; const rows: Record<string, unknown>[] = []; for (let i = 0; i < count; i++) { rows.push({ id: i + 1, name: names[i % names.length] + ' ' + (Math.floor(i / names.length) + 1), active: i % 3 !== 0, score: Math.floor(Math.random() * 100), created: new Date(Date.now() - i * 86400000), role: roles[i % roles.length], }); } return rows; }
let state = { rowCount: 100, columns: ['id', 'name', 'active', 'score', 'created', 'role'], fitMode: 'stretch', sortable: true, resizable: true, };
function rebuild() { const columns = state.columns.map((key) => ({ ...allColumnDefs[key], sortable: state.sortable, resizable: state.resizable, }));
grid.fitMode = state.fitMode; grid.gridConfig = { columns, sortable: state.sortable, resizable: state.resizable, typeDefaults: { date: { format: (val: Date) => val.toLocaleDateString(undefined, { day: '2-digit', month: '2-digit', year: 'numeric' }), }, }, features: { editing: 'dblclick' }, }; grid.rows = generateRows(state.rowCount); }
// Initial render rebuild();
// Re-render on control change container.addEventListener('control-change', ((e: CustomEvent) => { const { allValues } = e.detail; state = { rowCount: allValues.rowCount, columns: (allValues.columns) ?? state.columns, fitMode: (allValues.fitMode) ?? state.fitMode, sortable: allValues.sortable, resizable: allValues.resizable, }; rebuild(); })); }Keyboard Navigation
Section titled “Keyboard Navigation”The grid implements ARIA grid keyboard patterns out of the box — no configuration required:
| Key | Action |
|---|---|
| ↑ ↓ ← → | Move between cells |
| Home / End | Jump to first/last cell in row |
| Ctrl + Home / Ctrl + End | Jump to first/last cell in grid |
| PgUp / PgDn | Scroll by viewport height |
| ↵ Enter | Start editing (with EditingPlugin) |
| Esc | Cancel editing |
| ⇥ Tab / ⇧ Shift + ⇥ Tab | Move to next/previous editable cell |
For the full keyboard shortcut reference, see the Accessibility guide.
RTL (Right-to-Left) Support
Section titled “RTL (Right-to-Left) Support”The grid fully supports RTL languages like Hebrew, Arabic, and Persian. Set dir="rtl" on the grid or any ancestor element — keyboard navigation, column pinning, and layout all adapt automatically.
<tbw-grid dir="rtl"></tbw-grid><tbw-grid dir="rtl" style="height: 220px;"></tbw-grid> import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';
const grid = queryGrid('tbw-grid'); if (container && grid) { grid.columns = [ { field: 'name', header: 'الاسم', width: 150 }, { field: 'department', header: 'القسم', width: 130 }, { field: 'salary', header: 'الراتب', width: 120, formatter: (v: number) => `${v.toLocaleString('ar-EG')} ر.س` }, ]; grid.rows = [ { name: 'أحمد', department: 'الهندسة', salary: 42000 }, { name: 'فاطمة', department: 'التصميم', salary: 38000 }, { name: 'خالد', department: 'المبيعات', salary: 35000 }, { name: 'سارة', department: 'الهندسة', salary: 44000 }, { name: 'محمد', department: 'الدعم', salary: 31000 }, ];
container.addEventListener('control-change', ((e: CustomEvent) => { grid.dir = e.detail.allValues.rtl ? 'rtl' : 'ltr'; })); }Logical column pinning: Use pinned: 'start' and pinned: 'end' instead of 'left'/'right' for direction-independent pinning. See the Pinned Columns plugin for details.
Configuration
Section titled “Configuration”Column Inference
Section titled “Column Inference”Zero-config data display — Just pass your data and the grid figures out the rest.
When you provide rows without defining columns, the grid automatically:
- Detects fields from the first row’s property names
- Infers data types (
string,number,boolean,date) from actual values - Generates human-readable headers from field names (
firstName→First Name) - Applies appropriate sorting and formatting for each type
grid.rows = myData; // That's it!Pass data without defining columns — the grid infers everything automatically.
<tbw-grid style="height: 260px;"></tbw-grid> import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';
const grid = queryGrid('tbw-grid'); if (grid) { grid.rows = [ { id: 1, firstName: 'Alice', lastName: 'Johnson', age: 32, active: true, startDate: '2022-03-15' }, { id: 2, firstName: 'Bob', lastName: 'Smith', age: 28, active: false, startDate: '2023-01-10' }, { id: 3, firstName: 'Carol', lastName: 'Williams', age: 45, active: true, startDate: '2021-07-22' }, { id: 4, firstName: 'David', lastName: 'Brown', age: 36, active: true, startDate: '2020-11-05' }, { id: 5, firstName: 'Eve', lastName: 'Davis', age: 29, active: false, startDate: '2024-02-18' }, ]; }Light DOM Columns
Section titled “Light DOM Columns”Declarative configuration — Define columns in HTML instead of JavaScript.
Use <tbw-grid-column> elements (or framework wrapper components) to declaratively define columns directly in your markup:
<tbw-grid> <tbw-grid-column field="id" header="ID" width="80"></tbw-grid-column> <tbw-grid-column field="name" header="Full Name" sortable></tbw-grid-column> <tbw-grid-column field="email" header="Email Address"></tbw-grid-column></tbw-grid>import { DataGrid, GridColumn } from '@toolbox-web/grid-react';
function EmployeeGrid({ rows }) { return ( <DataGrid rows={rows}> <GridColumn field="id" header="ID" width={80} /> <GridColumn field="name" header="Full Name" sortable /> <GridColumn field="email" header="Email Address" /> </DataGrid> );}<script setup lang="ts">import { TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue';</script>
<template> <TbwGrid :rows="rows"> <TbwGridColumn field="id" header="ID" :width="80" /> <TbwGridColumn field="name" header="Full Name" sortable /> <TbwGridColumn field="email" header="Email Address" /> </TbwGrid></template><tbw-grid [rows]="rows"> <tbw-grid-column field="id" header="ID" width="80"></tbw-grid-column> <tbw-grid-column field="name" header="Full Name" sortable></tbw-grid-column> <tbw-grid-column field="email" header="Email Address"></tbw-grid-column></tbw-grid>This approach is ideal for:
- Static layouts where columns don’t change at runtime
- Server-rendered pages where HTML is generated on the server
- Template-driven frameworks like Angular or Vue that prefer declarative syntax
- Quick prototyping without writing JavaScript
Define columns declaratively in HTML using <tbw-grid-column> elements.
import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';
const grid = queryGrid('tbw-grid'); if (grid) { grid.rows = [ { id: 1, name: 'Alice Johnson', email: 'alice@example.com', department: 'Engineering' }, { id: 2, name: 'Bob Smith', email: 'bob@example.com', department: 'Sales' }, { id: 3, name: 'Carol Williams', email: 'carol@example.com', department: 'Marketing' }, { id: 4, name: 'David Brown', email: 'david@example.com', department: 'Engineering' }, { id: 5, name: 'Eve Davis', email: 'eve@example.com', department: 'HR' }, ]; }<tbw-grid style="height: 260px;"> <tbw-grid-column field="id" header="ID" width="80"></tbw-grid-column> <tbw-grid-column field="name" header="Full Name" sortable></tbw-grid-column> <tbw-grid-column field="email" header="Email Address"></tbw-grid-column> <tbw-grid-column field="department" header="Department" width="140"></tbw-grid-column> </tbw-grid>Configuration Reference
Section titled “Configuration Reference”The grid is configured through the gridConfig property (or individual shorthand properties). The table below covers the most common options — see GridConfig for the full, type-checked reference.
| Property | Type | Description |
|---|---|---|
columns | ColumnConfig[] | Column definitions |
rows | any[] | Row data array (top-level grid prop, not on GridConfig) |
fitMode | FitMode | How columns fill available width ('stretch' or 'fixed') |
sortable | boolean | Grid-wide sort toggle (default true) |
resizable | boolean | Grid-wide resize toggle (default true) |
initialSort | { field, direction } | Sort applied on first render ('asc' or 'desc') |
rowHeight | number | (row, index) => number | undefined | Fixed or variable row heights |
getRowId | (row) => string | Unique row identity function |
rowClass | (row) => string | string[] | Dynamic row CSS classes |
typeDefaults | Record<string, TypeDefault> | Default format/renderer per column type |
plugins | GridPlugin[] | Plugin instances |
features | Partial<FeatureConfig> | Declarative feature config (alternative to plugins) |
columnState | GridColumnState | Saved column state to restore on init |
shell | ShellConfig | Shell layout (header, tool panels) |
icons | GridIcons | Grid-wide icon overrides |
animation | AnimationConfig | Animation defaults (expand/collapse, reorder, etc.) |
loadingRenderer | LoadingRenderer | Custom loading indicator |
sortHandler | SortHandler | Low-level sort engine override (prefer sortComparator per-column) |
gridAriaLabel | string | Accessible label for the grid (aria-label) |
gridAriaDescribedBy | string | ID of an element that describes the grid (aria-describedby) |
a11y | A11yConfig | Screen reader announcement messages and toggle |
Precedence (low → high):
gridConfigprop (base)- Light DOM elements (declarative)
columnsprop (direct array)- Inferred columns (auto-detected from first row)
- Individual props (
fitMode) — highest
Presentation
Section titled “Presentation”Value Accessors
Section titled “Value Accessors”Resolve a cell’s value when it isn’t a plain field read. By default the grid reads row[field]. A valueAccessor is only relevant when that’s not the right value — typically because the cell is computed from multiple row fields, plucked from a nested structure, or derived from something outside the row.
You could achieve the visual result with a custom renderer or format function alone, but you’d lose every other feature that depends on knowing what the cell’s value actually is — sorting (unless you also write a sortComparator), filtering (unless you also write a filterValue), grouping, aggregations, copy-to-clipboard, and exports (CSV / Excel) would all see undefined or the raw row[field]. valueAccessor plugs the value in once and every consumer stays consistent.
grid.columns = [ // Computed from sibling fields { field: 'total', headerName: 'Total', valueAccessor: ({ row }) => row.qty * row.price, },
// Pluck from a nested array { field: 'lastShipmentDate', headerName: 'Last Shipment', valueAccessor: ({ row }) => row.shipments?.find((s) => s.kind === 'BL')?.date, format: (v) => (v ? new Date(v).toLocaleDateString() : '—'), },
// Normalize / coerce { field: 'name', valueAccessor: ({ row }) => `${row.firstName} ${row.lastName}`.trim(), },];The accessor receives { row, column, rowIndex } and returns the column’s typed value. Use it whenever the cell value isn’t a plain field read.
Precedence
Section titled “Precedence”For each operation, the grid looks for a column-level override first, then falls back to the accessor, then to the field:
| Operation | Order |
|---|---|
| Sort comparison | sortComparator → valueAccessor → row[field] |
| Filter value | filterValue → valueAccessor → row[field] |
Group key & aggregations (sum, avg, min, max, first, last) | valueAccessor → row[field] |
| Copy / export (CSV, Excel) | valueAccessor → row[field] (then format if present) |
Display (format / renderer) | valueAccessor → row[field] |
This means you write the lookup logic once and every consumer stays consistent.
Caching & invalidation
Section titled “Caching & invalidation”Accessor results are cached per (row, column.field) in a WeakMap keyed on the row object — so an expensive accessor (e.g. array.find) runs once per row regardless of how many features read it. Primitive rows bypass the cache.
The cache is invalidated automatically when:
- A row reference changes (immutable updates — recommended pattern).
- You call
RowManager.updateRow/updateRows/applyTransaction(in-place edits). - The Editing plugin commits a value.
If you mutate row data outside of those paths, call invalidateAccessorCache(row?, field?) manually:
import { invalidateAccessorCache } from '@toolbox-web/grid';
row.shipments.push(newShipment);invalidateAccessorCache(row, 'lastShipmentDate'); // narrow scope// or invalidateAccessorCache(row); // all fields on this row// or invalidateAccessorCache(); // entire cacheEditing
Section titled “Editing”Accessors are read-only. Cells driven by a valueAccessor cannot currently be written back through the Editing plugin (a matching valueSetter API is planned). For now, gate them with editable: false or omit editable.
Formatters
Section titled “Formatters”Transform how values are displayed — Formatters convert raw data values into user-friendly text.
A formatter is a function that receives the cell value and returns a display string:
grid.columns = [ // Currency { field: 'salary', format: (v) => `$${v.toLocaleString()}` },
// Date { field: 'hireDate', format: (v) => new Date(v).toLocaleDateString() },
// Percentage { field: 'progress', format: (v) => `${(v * 100).toFixed(1)}%` },
// Prefix { field: 'id', format: (v) => `#${v}` },];Row Styling
Section titled “Row Styling”Style entire rows based on data using the rowClass callback:
grid.gridConfig = { rowClass: (row) => (row.status === 'inactive' ? 'row-inactive' : ''),};Then define the CSS class in your stylesheet:
.row-inactive { opacity: 0.5; }Cell Styling
Section titled “Cell Styling”Style individual cells based on their value using cellClass on a column:
grid.columns = [ { field: 'score', cellClass: (value) => { if (value >= 90) return 'cell-success'; if (value < 50) return 'cell-danger'; return ''; }, },];
Use rowClass and cellClass to apply conditional styles based on data.
<tbw-grid style="height: 280px;"></tbw-grid> import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';
interface Employee { id: number; name: string; status: string; score: number; department: string; }
const grid = queryGrid<Employee>('tbw-grid'); if (grid) { grid.gridConfig = { rowClass: (row) => row.status === 'inactive' ? 'row-inactive' : '', columns: [ { field: 'id', header: 'ID', width: 60 }, { field: 'name', header: 'Name', width: 140 }, { field: 'department', header: 'Department', width: 120 }, { field: 'status', header: 'Status', width: 100 }, { field: 'score', header: 'Score', width: 100, align: 'right', cellClass: (value) => { if ((value) >= 90) return 'cell-success'; if ((value) < 50) return 'cell-danger'; if ((value) < 70) return 'cell-warning'; return ''; }, }, ], }; grid.rows = [ { id: 1, name: 'Alice', status: 'active', score: 95, department: 'Engineering' }, { id: 2, name: 'Bob', status: 'inactive', score: 42, department: 'Sales' }, { id: 3, name: 'Carol', status: 'active', score: 78, department: 'Marketing' }, { id: 4, name: 'David', status: 'active', score: 35, department: 'Engineering' }, { id: 5, name: 'Eve', status: 'active', score: 91, department: 'HR' }, { id: 6, name: 'Frank', status: 'inactive', score: 67, department: 'Finance' }, ]; }Renderers
Section titled “Renderers”Full control over cell content — Renderers let you create custom HTML elements for cells.
While formatters return plain text, renderers return DOM elements. Use renderers when you need:
- Custom components: Checkboxes, badges, progress bars, buttons
- Interactive elements: Links, icons, action buttons
- Rich formatting: Multiple elements, images, complex layouts
grid.columns = [ { field: 'status', header: 'Status', renderer: (ctx) => { const badge = document.createElement('span'); badge.className = `badge badge-${ctx.value}`; badge.textContent = ctx.value; return badge; }, }, { field: 'active', header: 'Active', renderer: (ctx) => { const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = !!ctx.value; checkbox.disabled = true; return checkbox; }, },];import { DataGrid, GridColumn } from '@toolbox-web/grid-react';
function EmployeeGrid({ rows }) { return ( <DataGrid rows={rows}> <GridColumn field="status" header="Status" renderer={({ value }) => ( <span className={`badge badge-${value}`}>{value}</span> )} /> <GridColumn field="active" header="Active" renderer={({ value }) => ( <input type="checkbox" checked={!!value} disabled /> )} /> </DataGrid> );}<script setup lang="ts">import { TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue';</script>
<template> <TbwGrid :rows="rows"> <TbwGridColumn field="status" header="Status"> <template #cell="{ value }"> <span :class="`badge badge-${value}`">{{ value }}</span> </template> </TbwGridColumn> <TbwGridColumn field="active" header="Active"> <template #cell="{ value }"> <input type="checkbox" :checked="!!value" disabled /> </template> </TbwGridColumn> </TbwGrid></template>import { Component } from '@angular/core';import { Grid, TbwRenderer } from '@toolbox-web/grid-angular';
@Component({ imports: [Grid, TbwRenderer], template: ` <tbw-grid [rows]="rows" style="height: 400px; display: block"> <tbw-grid-column field="status" header="Status"> <span *tbwRenderer="let value" [class]="'badge badge-' + value"> {{ value }} </span> </tbw-grid-column> <tbw-grid-column field="active" header="Active"> <input *tbwRenderer="let value" type="checkbox" [checked]="!!value" disabled /> </tbw-grid-column> </tbw-grid> `,})export class EmployeeGridComponent { rows = [/* ... */];}The renderer receives a CellRenderContext — the cell value, the row object, the field name, and the column config.
Renderers give full control over cell content — create badges, checkboxes, progress bars, and more.
<tbw-grid style="height: 280px;"></tbw-grid> import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';
interface Employee { id: number; name: string; status: string; active: boolean; rating: number; salary: number; }
const grid = queryGrid<Employee>('tbw-grid'); if (grid) { grid.columns = [ { field: 'id', header: 'ID', width: 60 }, { field: 'name', header: 'Name', width: 140 }, { field: 'status', header: 'Status', width: 110, renderer: ({ value, cellEl }) => { const colors: Record<string, string> = { active: '#16a34a', inactive: '#dc2626', 'on-leave': '#d97706', }; const badge = document.createElement('span'); badge.style.cssText = ` display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 0.8em; background: ${colors[value] || '#888'}22; color: ${colors[value] || '#888'}; font-weight: 600; `; badge.textContent = (value).charAt(0).toUpperCase() + (value).slice(1); return badge; }, }, { field: 'active', header: 'Active', width: 80, align: 'center', renderer: ({ value }) => { const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = !!value; checkbox.disabled = true; return checkbox; }, }, { field: 'rating', header: 'Rating', width: 100, align: 'center', renderer: ({ value }) => '★'.repeat(value) + '☆'.repeat(5 - (value)), }, { field: 'salary', header: 'Salary', width: 120, align: 'right', format: (value) => `$${(value).toLocaleString()}`, }, ]; grid.rows = [ { id: 1, name: 'Alice Johnson', status: 'active', active: true, rating: 5, salary: 95000 }, { id: 2, name: 'Bob Smith', status: 'inactive', active: false, rating: 3, salary: 72000 }, { id: 3, name: 'Carol Williams', status: 'on-leave', active: true, rating: 4, salary: 88000 }, { id: 4, name: 'David Brown', status: 'active', active: true, rating: 5, salary: 105000 }, { id: 5, name: 'Eve Davis', status: 'active', active: false, rating: 2, salary: 65000 }, ]; }Choosing: Formatter vs Renderer
Section titled “Choosing: Formatter vs Renderer”Now that you’ve seen both, here’s a quick reference for picking the right one:
Formatter (format) | Renderer (renderer) | |
|---|---|---|
| Returns | String | DOM element |
| Use for | Currency, dates, percentages, text formatting | Checkboxes, badges, buttons, links, images |
| Performance | Faster (text only) | Slower (DOM creation) |
| Interactivity | None (display only) | Full (event listeners, components) |
Rule of thumb: If you only need to change how a value looks, use a format function. If you need interactive elements or custom HTML structure, use a renderer.
Row Animation
Section titled “Row Animation”The grid provides a built-in row animation API for highlighting changes, insertions, and removals with visual feedback.
| Method | Description | CSS Variable |
|---|---|---|
animateRow(i, 'change') | Flash highlight for data changes | --tbw-row-change-duration (500ms) |
insertRow(i, row) | Slide-in animation for new rows | --tbw-row-insert-duration (300ms) |
removeRow(i) | Fade-out animation for removed rows | --tbw-row-remove-duration (200ms) |
applyTransaction(tx) | Animates add/update/remove in one pass | All of the above |
// Highlight a row after updatinggrid.rows[5].status = 'updated';grid.animateRow(5, 'change');
// Animate by row ID (stable when sorted/filtered)grid.animateRowById(data.id, 'change');
// Animate multiple rows at oncegrid.animateRows([0, 2, 5], 'change');Customize appearance:
tbw-grid { --tbw-row-change-duration: 750ms; --tbw-row-change-color: rgba(34, 197, 94, 0.25);}Built-in row animation API for highlighting changes, insertions, and removals.
<tbw-grid style="height: 260px;"></tbw-grid> import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';
interface Item { id: number; name: string; status: string; }
const grid = queryGrid<Item>('tbw-grid');
if (grid) { const container = document.querySelector('.grid-demo'); let nextId = 6; const data: Item[] = [ { id: 1, name: 'Alpha', status: 'active' }, { id: 2, name: 'Bravo', status: 'active' }, { id: 3, name: 'Charlie', status: 'pending' }, { id: 4, name: 'Delta', status: 'active' }, { id: 5, name: 'Echo', status: 'inactive' }, ];
grid.columns = [ { field: 'id', header: 'ID', width: 60 }, { field: 'name', header: 'Name', width: 140 }, { field: 'status', header: 'Status', width: 120 }, ]; grid.rows = data;
container.addEventListener('click', (e) => { const action = (e.target).dataset.animAction; if (!action) return;
if (action === 'change') { const idx = Math.floor(Math.random() * grid.rows.length); grid.animateRow(idx, 'change'); } else if (action === 'insert') { const id = nextId++; const row = { id, name: `New-${id}`, status: 'new' }; grid.insertRow(grid.rows.length, row); } else if (action === 'remove' && grid.rows.length > 1) { grid.removeRow(grid.rows.length - 1); } }); }Type-Level Defaults
Section titled “Type-Level Defaults”Define default formatters and renderers for all columns of a specific type:
grid.gridConfig = { typeDefaults: { currency: { format: (value) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', }).format(value), }, boolean: { renderer: (ctx) => { const cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = !!ctx.value; cb.disabled = true; return cb; }, }, status: { renderer: (ctx) => { const badge = document.createElement('span'); badge.className = `status-badge status-${ctx.value}`; badge.textContent = ctx.value; return badge; }, }, }, columns: [ { field: 'salary', type: 'currency' }, { field: 'active', type: 'boolean' }, { field: 'status', type: 'status' }, ],};import { GridTypeProvider, DataGrid, GridColumn } from '@toolbox-web/grid-react';import type { TypeDefaultsMap } from '@toolbox-web/grid-react';
const typeDefaults: TypeDefaultsMap = { currency: { format: (value) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value), }, boolean: { renderer: ({ value }) => <input type="checkbox" checked={!!value} disabled />, }, status: { renderer: ({ value }) => ( <span className={`status-badge status-${value}`}>{value}</span> ), },};
function App() { return ( <GridTypeProvider defaults={typeDefaults}> <DataGrid rows={rows}> <GridColumn field="salary" type="currency" /> <GridColumn field="active" type="boolean" /> <GridColumn field="status" type="status" /> </DataGrid> </GridTypeProvider> );}<script setup lang="ts">import { h } from 'vue';import { GridTypeProvider, TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue';import CheckboxCell from './CheckboxCell.vue';import StatusBadge from './StatusBadge.vue';
const typeDefaults = { currency: { format: (value) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value), }, boolean: { renderer: ({ value }) => h(CheckboxCell, { checked: !!value }), }, status: { renderer: ({ value }) => h(StatusBadge, { status: value }), },};</script>
<template> <GridTypeProvider :defaults="typeDefaults"> <TbwGrid :rows="rows"> <TbwGridColumn field="salary" type="currency" /> <TbwGridColumn field="active" type="boolean" /> <TbwGridColumn field="status" type="status" /> </TbwGrid> </GridTypeProvider></template>import { Component } from '@angular/core';import { Grid, provideGridTypeDefaults } from '@toolbox-web/grid-angular';import { CheckboxCellComponent } from './checkbox-cell.component';import { StatusBadgeComponent } from './status-badge.component';
@Component({ imports: [Grid], providers: [ provideGridTypeDefaults({ currency: { format: (value) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value), }, boolean: { renderer: CheckboxCellComponent }, status: { renderer: StatusBadgeComponent }, }), ], template: ` <tbw-grid [rows]="rows" [columns]="columns" /> `,})export class AppComponent { columns = [ { field: 'salary', type: 'currency' }, { field: 'active', type: 'boolean' }, { field: 'status', type: 'status' }, ];}Type defaults reduce repetition when many columns share the same presentation. Column-level format or renderer overrides type defaults when both are specified.
Custom Header Renderers
Section titled “Custom Header Renderers”Customize column header cells using headerLabelRenderer or headerRenderer.
headerLabelRenderer — Modify just the label portion of the header (sort icons and filter buttons are still managed by the grid):
{ field: 'name', header: 'Name', headerLabelRenderer: ({ value }) => { const span = document.createElement('span'); span.innerHTML = `${value} <span style="color:red">*</span>`; return span; },}// In GridConfig or ColumnConfig{ field: 'name', header: 'Name', headerLabelRenderer: ({ value }) => ( <span>{value} <span style={{ color: 'red' }}>*</span></span> ),}// In GridConfig or ColumnConfig{ field: 'name', header: 'Name', headerLabelRenderer: ({ value }) => h('span', [value, h('span', { style: 'color:red' }, ' *')]),}// Using a component class in ColumnConfig{ field: 'name', header: 'Name', headerLabelRenderer: RequiredLabelComponent,}
// RequiredLabelComponent@Component({ selector: 'app-required-label', template: `{{ value() }} <span style="color:red">*</span>`,})export class RequiredLabelComponent { value = input<string>(); column = input<unknown>();}headerRenderer — Full control over the entire header cell. You are responsible for rendering sort icons using ctx.renderSortIcon():
{ field: 'email', header: 'Email', headerRenderer: (ctx) => { const wrapper = document.createElement('div'); wrapper.style.cssText = 'display:flex;align-items:center;gap:6px;width:100%';
const icon = document.createElement('span'); icon.textContent = '📧'; wrapper.appendChild(icon);
const label = document.createElement('span'); label.textContent = ctx.value; label.style.flex = '1'; wrapper.appendChild(label);
const sortIcon = ctx.renderSortIcon(); if (sortIcon) wrapper.appendChild(sortIcon); return wrapper; },}// In GridConfig or ColumnConfig// Use ctx.sortState to render your own sort indicator in JSX{ field: 'email', header: 'Email', headerRenderer: (ctx) => ( <div style={{ display: 'flex', alignItems: 'center', gap: '6px', width: '100%' }}> <span>📧</span> <span style={{ flex: 1 }}>{ctx.value}</span> {ctx.sortState && ( <span className="sort-icon"> {ctx.sortState === 'asc' ? '▲' : '▼'} </span> )} </div> ),}// In GridConfig or ColumnConfig// Use ctx.sortState to render your own sort indicator{ field: 'email', header: 'Email', headerRenderer: (ctx) => h('div', { style: 'display:flex;align-items:center;gap:6px;width:100%' }, [ h('span', '📧'), h('span', { style: 'flex:1' }, ctx.value), ctx.sortState ? h('span', { class: 'sort-icon' }, ctx.sortState === 'asc' ? '▲' : '▼') : null, ]),}// Using a component class in ColumnConfig{ field: 'email', header: 'Email', headerRenderer: EmailHeaderComponent,}
// EmailHeaderComponent@Component({ selector: 'app-email-header', template: ` <div style="display:flex;align-items:center;gap:6px;width:100%"> <span>📧</span> <span style="flex:1">{{ value() }}</span> </div> `,})export class EmailHeaderComponent { value = input<string>(); column = input<unknown>(); sortState = input<'asc' | 'desc' | null>(); filterActive = input<boolean>(); renderSortIcon = input<() => HTMLElement | null>(); renderFilterButton = input<() => HTMLElement | null>();}The HeaderCellContext gives you the label text, the column config, the current sort/filter state, and helpers to render the built-in sort icon and filter button when you want to keep them.
Name uses headerLabelRenderer (adds a red asterisk).
Email uses headerRenderer (full control with custom icon + sort icon).
<tbw-grid style="height: 260px;"></tbw-grid> import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';
const grid = queryGrid('tbw-grid');
if (grid) { grid.columns = [ { field: 'id', header: 'ID', sortable: true, width: 60 }, { field: 'name', header: 'Name', sortable: true, resizable: true, headerLabelRenderer: ({ value }) => { const span = document.createElement('span'); span.innerHTML = `${value} <span style="color:red;font-weight:bold;">*</span>`; return span; }, }, { field: 'email', header: 'Email', sortable: true, resizable: true, headerRenderer: (ctx) => { const wrapper = document.createElement('div'); wrapper.style.cssText = 'display:flex;align-items:center;gap:6px;width:100%;'; const icon = document.createElement('span'); icon.textContent = '📧'; wrapper.appendChild(icon); const label = document.createElement('span'); label.textContent = ctx.value; label.style.flex = '1'; wrapper.appendChild(label); const sortIcon = ctx.renderSortIcon(); if (sortIcon) wrapper.appendChild(sortIcon); return wrapper; }, }, { field: 'score', header: 'Score', sortable: true, type: 'number', width: 80 }, ];
grid.rows = [ { id: 1, name: 'Alice', email: 'alice@example.com', score: 85 }, { id: 2, name: 'Bob', email: 'bob@example.com', score: 72 }, { id: 3, name: 'Carol', email: 'carol@example.com', score: 91 }, { id: 4, name: 'Dan', email: 'dan@example.com', score: 68 }, { id: 5, name: 'Eve', email: 'eve@example.com', score: 95 }, ]; }Column State Persistence
Section titled “Column State Persistence”The grid tracks column state — widths, sort direction, order, and visibility. You can save, load, and reset this state for user personalization.
What State Contains
Section titled “What State Contains”getColumnState() returns an array of GridColumnState objects — one entry per column capturing its field, current width, sort direction and priority, and visibility.
Listening for Changes
Section titled “Listening for Changes”The column-state-change event fires whenever the user resizes, reorders, sorts, or hides/shows columns:
grid.on('column-state-change', (state) => { localStorage.setItem('my-grid-state', JSON.stringify(state));});<DataGrid rows={rows} onColumnStateChange={(e) => { localStorage.setItem('my-grid-state', JSON.stringify(e.detail)); }}/><template> <TbwGrid :rows="rows" @column-state-change="onColumnStateChange" /></template>
<script setup lang="ts">function onColumnStateChange(e: CustomEvent<GridColumnState[]>) { localStorage.setItem('my-grid-state', JSON.stringify(e.detail));}</script><tbw-grid [rows]="rows" (column-state-change)="onColumnStateChange($event)" />onColumnStateChange(e: CustomEvent<GridColumnState[]>) { localStorage.setItem('my-grid-state', JSON.stringify(e.detail));}Applying Saved State
Section titled “Applying Saved State”Restore a previously saved state using applyColumnState():
const saved = localStorage.getItem('my-grid-state');if (saved) { grid.applyColumnState(JSON.parse(saved));}Resetting State
Section titled “Resetting State”Re-assign the original column definitions to reset to defaults:
grid.columns = [...originalColumns];<tbw-grid style="height: 280px;"></tbw-grid> import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';
const STORAGE_KEY = 'tbw-demo-column-state';
const container = document.getElementById('demo-column-state')?.closest('.grid-demo'); if (container) { const grid = queryGrid('tbw-grid');
const status = container.querySelector<HTMLElement>('[data-status]'); const log = container.querySelector<HTMLElement>('[data-log]');
const defaultColumns: ColumnConfig[] = [ { field: 'id', header: 'ID', width: 60 }, { field: 'name', header: 'Name', width: 160 }, { field: 'department', header: 'Department', width: 140 }, { field: 'salary', header: 'Salary', type: 'number', width: 100 }, { field: 'location', header: 'Location', width: 130 }, ];
grid.columns = defaultColumns; grid.sortable = true; grid.resizable = true; grid.rows = [ { id: 1, name: 'Alice Johnson', department: 'Engineering', salary: 95000, location: 'New York' }, { id: 2, name: 'Bob Smith', department: 'Marketing', salary: 72000, location: 'London' }, { id: 3, name: 'Carol Williams', department: 'Engineering', salary: 108000, location: 'Berlin' }, { id: 4, name: 'Dan Brown', department: 'Sales', salary: 67000, location: 'Tokyo' }, { id: 5, name: 'Eve Davis', department: 'Marketing', salary: 81000, location: 'New York' }, { id: 6, name: 'Frank Miller', department: 'Sales', salary: 73000, location: 'London' }, ];
function showStatus(msg: string) { status.textContent = msg; setTimeout(() => { status.textContent = ''; }, 2000); }
function appendLog(msg: string) { log.textContent += msg + '\n'; log.scrollTop = log.scrollHeight; }
grid.on('column-state-change', (detail) => { appendLog(`State changed: ${JSON.stringify(detail).slice(0, 120)}…`); });
container.querySelector('[data-save]').addEventListener('click', () => { const state = grid.getColumnState(); localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); showStatus('State saved!'); appendLog('Saved: ' + JSON.stringify(state, null, 2)); });
container.querySelector('[data-load]').addEventListener('click', () => { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) { showStatus('No saved state found.'); return; } const state: GridColumnState[] = JSON.parse(raw); grid.applyColumnState(state); showStatus('State loaded!'); appendLog('Loaded: ' + JSON.stringify(state, null, 2)); });
container.querySelector('[data-clear]').addEventListener('click', () => { localStorage.removeItem(STORAGE_KEY); showStatus('Saved state cleared.'); appendLog('Cleared localStorage.'); });
container.querySelector('[data-reset]').addEventListener('click', () => { grid.columns = [...defaultColumns]; showStatus('Grid reset to defaults.'); appendLog('Reset to default columns.'); }); }Shell Components
Section titled “Shell Components”The shell wraps the grid with an optional header bar (title + toolbar) and a collapsible tool panel sidebar.
Basic Shell
Section titled “Basic Shell”Enable the shell by providing a shell.header.title. Features like visibility automatically register a tool panel when the shell is active.
import '@toolbox-web/grid/features/visibility';
grid.gridConfig = { shell: { header: { title: 'Employee Data' } }, features: { visibility: true },};<tbw-grid style="height: 420px;"></tbw-grid> import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/visibility';
const grid = queryGrid('tbw-grid');
if (container && grid) { const departments = ['Engineering', 'Sales', 'Marketing', 'Support']; const names = ['Alice', 'Bob', 'Carol', 'Dan', 'Eve', 'Frank', 'Grace', 'Henry']; function generateRows(count: number) { return Array.from({ length: count }, (_, i) => ({ id: i + 1, name: names[i % names.length] + ' ' + (Math.floor(i / names.length) + 1), department: departments[i % departments.length], salary: 50000 + Math.floor(Math.random() * 50000), active: i % 3 !== 0, })); }
const shellColumns: ColumnConfig[] = [ { field: 'id', header: 'ID', type: 'number', width: 80 }, { field: 'name', header: 'Name', minWidth: 150 }, { field: 'department', header: 'Department', width: 150 }, { field: 'salary', header: 'Salary', type: 'number', width: 120 }, { field: 'active', header: 'Active', type: 'boolean', width: 80 }, ]; const sampleData = generateRows(20);
interface ShellValues { showTitle: boolean; showHeaderContent: boolean; showToolbarButton: boolean; showVisibilityPlugin: boolean; showCustomPanel: boolean; panelPosition: string; }
function rebuild(v: ShellValues) { const shellConfig: ShellConfig = { header: v.showTitle ? { title: 'Employee Data' } : {}, toolPanel: { position: v.panelPosition }, }; grid.gridConfig = { shell: shellConfig, columns: shellColumns, features: { ...(v.showVisibilityPlugin ? { visibility: true } : {}), }, }; grid.rows = sampleData;
if (v.showHeaderContent) { grid.registerHeaderContent({ id: 'row-count', order: 10, render: (el) => { const span = document.createElement('span'); span.style.cssText = 'font-size:13px;color:var(--sl-color-gray-3);padding:4px 8px;background:var(--sl-color-gray-6);border-radius:4px;'; const update = () => { span.textContent = `${grid.rows.length} rows`; }; const unsub = grid.on('data-change', update); el.appendChild(span); return () => { unsub(); span.remove(); }; }, }); } else { grid.unregisterHeaderContent('row-count'); }
if (v.showCustomPanel) { grid.registerToolPanel({ id: 'custom-info', icon: 'ℹ', tooltip: 'Info Panel', render: (el) => { el.innerHTML = '<div style="padding:16px;"><h4 style="margin:0 0 8px;">Custom Panel</h4><p style="margin:0;font-size:13px;">This panel was added via registerToolPanel().</p></div>'; return () => { el.innerHTML = ''; }; }, }); } else { grid.unregisterToolPanel('custom-info'); }
if (v.showToolbarButton) { grid.registerToolbarContent({ id: 'refresh-btn', render: (el) => { const btn = document.createElement('button'); btn.className = 'tbw-toolbar-btn'; btn.title = 'Refresh Data'; btn.setAttribute('aria-label', 'Refresh Data'); btn.textContent = '↻'; btn.addEventListener('click', () => { grid.rows = generateRows(20); }); el.appendChild(btn); return () => btn.remove(); }, }); } else { grid.unregisterToolbarContent('refresh-btn'); } }
rebuild({ showTitle: true, showHeaderContent: true, showToolbarButton: true, showVisibilityPlugin: true, showCustomPanel: false, panelPosition: 'right', });
container.addEventListener('control-change', ((e: CustomEvent) => { rebuild(e.detail.allValues); })); }Light DOM Configuration
Section titled “Light DOM Configuration”Configure the shell declaratively using <tbw-grid-header> and <tbw-grid-header-content> elements:
<tbw-grid> <tbw-grid-header title="Employee Directory"> <tbw-grid-header-content> <span id="row-count">0 employees</span> </tbw-grid-header-content> </tbw-grid-header></tbw-grid>
Shell configured via <tbw-grid-header> light DOM elements — no JavaScript shell config needed.
import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';
const grid = queryGrid('tbw-grid'); const departments = ['Engineering', 'Sales', 'Marketing', 'Support']; const names = ['Alice', 'Bob', 'Carol', 'Dan', 'Eve', 'Frank', 'Grace', 'Henry'];
grid.columns = [ { field: 'id', header: 'ID', type: 'number', width: 80 }, { field: 'name', header: 'Name', minWidth: 150 }, { field: 'department', header: 'Department', width: 150 }, { field: 'salary', header: 'Salary', type: 'number', width: 120 }, { field: 'active', header: 'Active', type: 'boolean', width: 80 }, ];
grid.rows = Array.from({ length: 20 }, (_, i) => ({ id: i + 1, name: names[i % names.length] + ' ' + (Math.floor(i / names.length) + 1), department: departments[i % departments.length], salary: 50000 + Math.floor(Math.random() * 50000), active: i % 3 !== 0, }));<tbw-grid style="height: 420px;"> <tbw-grid-header title="Employee Directory"> <tbw-grid-header-content> <span style="color: var(--sl-color-gray-3); font-size: 13px; padding: 4px 8px; background: var(--sl-color-gray-6); border-radius: 4px;">20 employees</span> </tbw-grid-header-content> </tbw-grid-header> </tbw-grid>Multiple Tool Panels
Section titled “Multiple Tool Panels”Register multiple tool panels — each gets a tab in the sidebar. Panels can be registered via configuration or the runtime registerToolPanel() API.
grid.registerToolPanel({ id: 'filter-panel', title: 'Filters', icon: '🔍', render(container) { container.innerHTML = '<p>Filter controls here</p>'; },});import { DataGrid, GridToolPanel } from '@toolbox-web/grid-react';
function MyGrid({ rows }) { return ( <DataGrid rows={rows} gridConfig={config}> <GridToolPanel id="filter-panel" title="Filters" icon="🔍"> {({ grid }) => <p>Filter controls here</p>} </GridToolPanel> </DataGrid> );}<script setup lang="ts">import { TbwGrid, TbwGridToolPanel } from '@toolbox-web/grid-vue';</script>
<template> <TbwGrid :rows="rows" :grid-config="config"> <TbwGridToolPanel id="filter-panel" label="Filters" icon="🔍"> <template #default="{ grid }"> <p>Filter controls here</p> </template> </TbwGridToolPanel> </TbwGrid></template><tbw-grid [rows]="rows" [gridConfig]="config"> <tbw-grid-tool-panel id="filter-panel" title="Filters" icon="🔍"> <ng-template> <p>Filter controls here</p> </ng-template> </tbw-grid-tool-panel></tbw-grid>Multiple tool panels (Columns, Filter, Settings). Click toolbar icons to toggle each.
<tbw-grid style="height: 420px;"></tbw-grid> import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/visibility';
const grid = queryGrid('tbw-grid');
if (grid) { const departments = ['Engineering', 'Sales', 'Marketing', 'Support']; const names = ['Alice', 'Bob', 'Carol', 'Dan', 'Eve', 'Frank', 'Grace', 'Henry'];
grid.gridConfig = { shell: { header: { title: 'Multi-Panel Demo' } }, columns: [ { field: 'id', header: 'ID', type: 'number', width: 80 }, { field: 'name', header: 'Name', minWidth: 150 }, { field: 'department', header: 'Department', width: 150 }, { field: 'salary', header: 'Salary', type: 'number', width: 120 }, { field: 'active', header: 'Active', type: 'boolean', width: 80 }, ], features: { visibility: true }, };
grid.rows = Array.from({ length: 20 }, (_, i) => ({ id: i + 1, name: names[i % names.length] + ' ' + (Math.floor(i / names.length) + 1), department: departments[i % departments.length], salary: 50000 + Math.floor(Math.random() * 50000), active: i % 3 !== 0, }));
// Custom filter panel grid.registerToolPanel({ id: 'filter', title: 'Filter', icon: '🔍', tooltip: 'Filter data', order: 20, render: (el) => { el.innerHTML = ` <div style="padding:0.75rem;"> <div style="margin-bottom:16px;"> <label style="display:block;margin-bottom:4px;font-size:12px;color:var(--sl-color-gray-3);">Name contains</label> <input type="text" placeholder="Search..." style="width:100%;padding:6px 8px;border:1px solid var(--sl-color-gray-5);border-radius:4px;box-sizing:border-box;background:var(--sl-color-gray-6);color:var(--sl-color-text);" /> </div> <div style="margin-bottom:16px;"> <label style="display:block;margin-bottom:4px;font-size:12px;color:var(--sl-color-gray-3);">Department</label> <select style="width:100%;padding:6px 8px;border:1px solid var(--sl-color-gray-5);border-radius:4px;box-sizing:border-box;background:var(--sl-color-gray-6);color:var(--sl-color-text);"> <option value="">All</option> <option value="Engineering">Engineering</option> <option value="Sales">Sales</option> <option value="Marketing">Marketing</option> <option value="Support">Support</option> </select> </div> </div> `; return () => { el.innerHTML = ''; }; }, });
// Custom settings panel grid.registerToolPanel({ id: 'settings', title: 'Settings', icon: '⚙', tooltip: 'Grid settings', order: 50, render: (el) => { el.innerHTML = ` <div style="padding:0.75rem;"> <label style="display:flex;align-items:center;gap:8px;margin-bottom:12px;"><input type="checkbox" checked /><span>Row hover effect</span></label> <label style="display:flex;align-items:center;gap:8px;margin-bottom:12px;"><input type="checkbox" checked /><span>Alternating row colors</span></label> <label style="display:flex;align-items:center;gap:8px;"><input type="checkbox" /><span>Compact mode</span></label> </div> `; return () => { el.innerHTML = ''; }; }, }); }Toolbar Buttons
Section titled “Toolbar Buttons”Add custom buttons to the shell toolbar using framework wrapper components or the registerToolbarContent() API:
<tbw-grid> <tbw-grid-tool-buttons> <button onclick="exportData()">📥 Export</button> <button onclick="printGrid()">🖨️ Print</button> </tbw-grid-tool-buttons></tbw-grid>import { DataGrid, GridToolButtons } from '@toolbox-web/grid-react';
function MyGrid({ rows }) { return ( <DataGrid rows={rows} gridConfig={config}> <GridToolButtons> <button onClick={exportData}>📥 Export</button> <button onClick={printGrid}>🖨️ Print</button> </GridToolButtons> </DataGrid> );}<script setup lang="ts">import { TbwGrid, TbwGridToolButtons } from '@toolbox-web/grid-vue';</script>
<template> <TbwGrid :rows="rows" :grid-config="config"> <TbwGridToolButtons> <button @click="exportData">📥 Export</button> <button @click="printGrid">🖨️ Print</button> </TbwGridToolButtons> </TbwGrid></template><tbw-grid [rows]="rows" [gridConfig]="config"> <tbw-grid-tool-buttons> <button (click)="exportData()">📥 Export</button> <button (click)="printGrid()">🖨️ Print</button> </tbw-grid-tool-buttons></tbw-grid>Custom toolbar buttons via light DOM. The grid provides the container — you provide the HTML.
import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';
const grid = queryGrid('tbw-grid');
if (grid && wrap) { const names = ['Alice', 'Bob', 'Carol', 'Dan', 'Eve', 'Frank', 'Grace', 'Henry']; const depts = ['Engineering', 'Sales', 'Marketing', 'Support'];
grid.columns = [ { field: 'id', header: 'ID', type: 'number', width: 80 }, { field: 'name', header: 'Name', minWidth: 150 }, { field: 'department', header: 'Department', width: 150 }, { field: 'salary', header: 'Salary', type: 'number', width: 120 }, ];
grid.rows = Array.from({ length: 15 }, (_, i) => ({ id: i + 1, name: names[i % names.length] + ' ' + (Math.floor(i / names.length) + 1), department: depts[i % depts.length], salary: 50000 + Math.floor(Math.random() * 50000), }));
const exportBtn = document.querySelector('[title="Export"]'); exportBtn?.addEventListener('click', () => alert('Export clicked!'));
const printBtn = document.querySelector('[title="Print"]'); printBtn?.addEventListener('click', () => alert('Print clicked!')); }<tbw-grid style="height: 380px;"> <tbw-grid-header title="Toolbar Demo"></tbw-grid-header> <tbw-grid-tool-buttons> <button class="tbw-toolbar-btn" title="Export" aria-label="Export">📥</button> <button class="tbw-toolbar-btn" title="Print" aria-label="Print">🖨️</button> </tbw-grid-tool-buttons> </tbw-grid>Shell Configuration Reference:
| Option | Type | Default | Description |
|---|---|---|---|
shell.header.title | string | — | Title text in the header bar |
shell.toolPanel.position | 'left' | 'right' | 'right' | Sidebar position |
shell.toolPanel.width | number | 280 | Sidebar width in pixels |
shell.toolPanel.defaultOpen | string | — | Panel ID to open on load |
shell.toolPanel.persistState | boolean | false | Remember open/closed state |
shell.toolPanel.closeOnClickOutside | boolean | false | Close on outside click |
Loading States
Section titled “Loading States”The grid supports loading indicators at three levels: grid-wide, per-row, and per-cell.
API Reference
Section titled “API Reference”Grid-level — Shows a full overlay spinner:
grid.loading = true;// ... fetch data ...grid.loading = false;The loading attribute also works in HTML: <tbw-grid loading></tbw-grid>.
Row-level — Shows a spinner on a specific row (requires getRowId):
grid.setRowLoading('row-42', true);// ... update row ...grid.setRowLoading('row-42', false);Cell-level — Shows a spinner on a specific cell:
grid.setCellLoading('row-42', 'status', true);// ... update cell ...grid.setCellLoading('row-42', 'status', false);Query and clear:
grid.isRowLoading('row-42'); // booleangrid.isCellLoading('row-42', 'name'); // booleangrid.clearAllLoading(); // remove all indicatorsClick ▶ Simulate to show the grid loading overlay.
<tbw-grid style="height: 300px;"></tbw-grid> import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';
interface Employee { id: string; name: string; email: string; department: string; }
const grid = queryGrid<Employee>('tbw-grid');
if (container && grid) { const names = ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve', 'Frank', 'Grace', 'Henry']; const departments = ['Engineering', 'Sales', 'Marketing', 'HR', 'Finance']; const employees: Employee[] = Array.from({ length: 8 }, (_, i) => ({ id: `emp-${i + 1}`, name: `${names[i % names.length]} ${i + 1}`, email: `${names[i % names.length].toLowerCase()}${i + 1}@example.com`, department: departments[i % departments.length], }));
grid.gridConfig = { getRowId: (row) => row.id }; grid.columns = [ { field: 'id', header: 'ID', width: 80 }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' }, ]; grid.rows = employees;
const hints: Record<string, string> = { grid: 'Click <strong>▶ Simulate</strong> to show full-grid loading overlay.', row: 'Click any <strong>row</strong> to trigger row loading.', cell: 'Click any <strong>cell</strong> to trigger cell loading.', }; const hintEl = document.querySelector('.loading-hint');
let currentMode: 'grid' | 'row' | 'cell' = 'grid'; let currentAutoReset = true;
container.addEventListener('control-change', ((e: CustomEvent) => { const v = e.detail.allValues; currentMode = v.mode; currentAutoReset = v.autoReset; if (hintEl) hintEl.innerHTML = hints[currentMode] ?? ''; }));
// Row click → row loading grid.on('row-click', ({ row }) => { if (currentMode !== 'row') return; const timeout = currentAutoReset ? 1000 : null; if (timeout) { grid.setRowLoading(row.id, true); setTimeout(() => grid.setRowLoading(row.id, false), timeout); } else { grid.setRowLoading(row.id, !grid.isRowLoading(row.id)); } });
// Cell click → cell loading grid.on('cell-click', ({ row, column }) => { if (currentMode !== 'cell') return; const timeout = currentAutoReset ? 1000 : null; if (timeout) { grid.setCellLoading(row.id, column.field, true); setTimeout(() => grid.setCellLoading(row.id, column.field, false), timeout); } else { grid.setCellLoading(row.id, column.field, !grid.isCellLoading(row.id, column.field)); } });
// Simulate button document.querySelector('[data-loading="simulate"]')?.addEventListener('click', async () => { grid.clearAllLoading(); const timeout = currentAutoReset ? 1000 : null;
if (currentMode === 'grid') { grid.loading = true; if (timeout) setTimeout(() => { grid.loading = false; }, timeout); } else if (currentMode === 'row') { for (const emp of employees) { grid.setRowLoading(emp.id, true); await new Promise((r) => setTimeout(r, 100)); } if (timeout) setTimeout(() => grid.clearAllLoading(), timeout); } else if (currentMode === 'cell') { for (const emp of employees) { grid.setCellLoading(emp.id, 'email', true); await new Promise((r) => setTimeout(r, 100)); } if (timeout) setTimeout(() => grid.clearAllLoading(), timeout); } }); }Custom Loading Renderer
Section titled “Custom Loading Renderer”Replace the default spinner with a custom element using loadingRenderer:
grid.gridConfig = { loadingRenderer: (context) => { const el = document.createElement('div'); el.className = 'my-spinner'; // context.size is 'large' (grid-level) or 'small' (row/cell) el.style.width = context.size === 'large' ? '48px' : '16px'; el.style.height = el.style.width; return el; },};import { DataGrid } from '@toolbox-web/grid-react';import type { GridConfig } from '@toolbox-web/grid-react';
const config: GridConfig = { loadingRenderer: ({ size }) => ( <div className="my-spinner" style={{ width: size === 'large' ? '48px' : '16px', height: size === 'large' ? '48px' : '16px', }} /> ),};
function MyGrid({ rows }) { return <DataGrid rows={rows} gridConfig={config} />;}<script setup lang="ts">import { h } from 'vue';import { TbwGrid } from '@toolbox-web/grid-vue';import type { GridConfig } from '@toolbox-web/grid-vue';
// Option 1: Render functionconst config: GridConfig = { loadingRenderer: ({ size }) => h('div', { class: 'my-spinner', style: { width: size === 'large' ? '48px' : '16px', height: size === 'large' ? '48px' : '16px', }, }),};
// Option 2: Vue component (receives `size` prop)// import MySpinner from './MySpinner.vue';// const config: GridConfig = { loadingRenderer: MySpinner };</script>
<template> <TbwGrid :rows="rows" :grid-config="config" /></template>import { Component, input } from '@angular/core';import { Grid } from '@toolbox-web/grid-angular';import type { GridConfig } from '@toolbox-web/grid-angular';
// Loading spinner component (receives `size` input)@Component({ selector: 'app-spinner', template: ` <div class="my-spinner" [style.width]="size() === 'large' ? '48px' : '16px'" [style.height]="size() === 'large' ? '48px' : '16px'"> </div> `,})export class SpinnerComponent { size = input<'large' | 'small'>('large');}
@Component({ imports: [Grid], template: `<tbw-grid [rows]="rows" [gridConfig]="config" />`,})export class MyGridComponent { config: GridConfig = { loadingRenderer: SpinnerComponent, };}The LoadingContext provides a size property: 'large' for the grid-wide overlay (up to 48×48 px) and 'small' for row/cell indicators (sized to the row height).
Replace the default spinner with a custom linear progress bar. Click Toggle to switch loading state.
<tbw-grid style="height: 300px;"></tbw-grid>import { queryGrid } from '@toolbox-web/grid';
const grid = await queryGrid('tbw-grid');const toggleBtn = document.querySelector<HTMLButtonElement>('[data-toggle="loading"]');
if (grid) { grid.columns = [ { field: 'id', header: 'ID', width: 80 }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'department', header: 'Department' }, ];
grid.rows = [ { id: 1, name: 'Alice Johnson', email: 'alice@example.com', department: 'Engineering' }, { id: 2, name: 'Bob Smith', email: 'bob@example.com', department: 'Marketing' }, { id: 3, name: 'Carol Davis', email: 'carol@example.com', department: 'Engineering' }, { id: 4, name: 'Dan Wilson', email: 'dan@example.com', department: 'Sales' }, { id: 5, name: 'Eve Brown', email: 'eve@example.com', department: 'HR' }, ];
grid.registerStyles('custom-loading', ` .progress-bar-container { position: absolute; top: 0; left: 0; right: 0; height: 4px; background: light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.08)); overflow: hidden; z-index: 1000; } .progress-bar { height: 100%; background: light-dark(#1976d2, #64b5f6); width: 30%; animation: progress-indeterminate 2s cubic-bezier(0.4,0,0.2,1) infinite; transform-origin: left; } @keyframes progress-indeterminate { 0% { transform: translateX(-100%); } 100% { transform: translateX(400%); } } `);
grid.gridConfig = { getRowId: (row: { id: number }) => row.id, loadingRenderer: () => { const container = document.createElement('div'); container.className = 'progress-bar-container'; const bar = document.createElement('div'); bar.className = 'progress-bar'; container.appendChild(bar); return container; }, };
toggleBtn?.addEventListener('click', () => { grid.loading = !grid.loading; });}Variable Row Heights
Section titled “Variable Row Heights”By default all rows share a fixed height (--tbw-row-height, default 28 px). You can configure per-row heights using a function.
Configuration
Section titled “Configuration”// Fixed height for all rowsgrid.gridConfig = { rowHeight: 56 };
// Per-row height functiongrid.gridConfig = { rowHeight: (row, index) => (row.hasDetails ? 80 : 40),};If your function returns undefined for a row, the grid auto-measures that row’s actual DOM height after rendering.
How Auto-Measurement Works
Section titled “How Auto-Measurement Works”- The grid renders rows with an estimated height.
- After paint, it reads
offsetHeightfor each rendered row. - Measured heights are cached and the position cache is rebuilt.
- A
ResizeObserverwatches for late layout shifts (font loading, lazy images) and re-measures automatically.
Row Identity for Height Caching
Section titled “Row Identity for Height Caching”Measured heights are cached using:
getRowId/rowId— preferred, survives sort/filter changes- Object reference (WeakMap) — fallback when no ID is configured
Provide getRowId for best results when rows are re-sorted, filtered, or grouped.
Plugin-Provided Heights
Section titled “Plugin-Provided Heights”Plugins can override row heights by implementing the getRowHeight(row, index) hook. The MasterDetailPlugin and ResponsivePlugin use this to provide expanded-row heights.
Performance Considerations
Section titled “Performance Considerations”- Fixed heights are fastest — the grid can calculate positions with pure math.
- Function-based heights add a per-row function call during position-cache rebuilds.
- Auto-measured heights require a DOM read pass after rendering — avoid for 10 k+ row grids unless combined with
getRowIdcaching. - When mixing fixed and auto-measured rows, return a number for most rows and
undefinedonly for rows that need measurement.
<tbw-grid></tbw-grid>import '@toolbox-web/grid';import { queryGrid, type CellRenderContext } from '@toolbox-web/grid';
interface Employee { id: number; name: string; role: string; department: string; notes: string; }
const grid = queryGrid<Employee>('tbw-grid');
grid.gridConfig = { columns: [ { field: 'id', header: 'ID', width: 60 }, { field: 'name', header: 'Name', width: 140 }, { field: 'role', header: 'Role', width: 100 }, { field: 'department', header: 'Department', width: 120 }, { field: 'notes', header: 'Notes', renderer: (ctx: CellRenderContext<Employee>) => { const { row } = ctx; // For demonstration, render notes with a tooltip for tall rows const cell = document.createElement('div'); cell.textContent = row.notes; if (tallRowIds.has(row.id)) { cell.title = 'This row has extended notes'; cell.style.whiteSpace = 'pre-wrap'; cell.style.fontStyle = 'italic'; } return cell; } }, ], getRowId: (row) => String(row.id), rowHeight: (row) => { // Rows with ids in tallRowIds get a taller height if (tallRowIds.has(row.id)) return 56; return undefined; // use default height }, };
// Generate 150 rows — a handful have long notes that warrant taller rows const tallRowIds = new Set([5, 18, 42, 77, 130]); const departments = ['Engineering', 'Marketing', 'Sales', 'Support', 'Finance', 'HR', 'Legal', 'Design']; const roles = ['Manager', 'Senior', 'Junior', 'Lead', 'Intern', 'Director', 'VP', 'Analyst'];
grid.rows = Array.from({ length: 150 }, (_, i) => { const id = i + 1; const isTall = tallRowIds.has(id); return { id, name: `Employee ${id}`, role: roles[i % roles.length], department: departments[i % departments.length], notes: isTall ? `This employee has extended notes that require a taller row. ` + `Additional context: performance review pending, cross-team ` + `collaboration active, mentoring two junior engineers.` : `Standard note for employee ${id}.`, }; });Events
Section titled “Events”The grid dispatches standard CustomEvents for user interactions — clicks, sort changes, column resizes, and more. Plugin-specific events (selection, editing, filtering, etc.) are also available when those plugins are active.
For the complete event reference — including listening patterns per framework, cancelable events, and all plugin events — see the Events section of the API Reference.
Methods
Section titled “Methods”The <tbw-grid> element exposes public methods for programmatic control — data manipulation, focus management, column state persistence, custom styles, shell control, loading indicators, row animation, and more. Use createGrid() or queryGrid() for type-safe access.
For the complete method reference — including signatures, return types, and factory functions — see the API Reference page.
See Also
Section titled “See Also”- Getting Started — Installation and setup
- Common Patterns — Full application recipes
- Plugins — Extend with selection, filtering, editing, and more
- Theming — CSS custom properties, dark mode, themes
- Architecture — Render scheduler, Light DOM, design decisions
- Plugin System — Build your own plugins