Skip to content

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.


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.

Row count Number of rows to generate
Columns Visible columns
Fit mode Column sizing strategy
Sortable Enable column sorting
Resizable Enable column resizing

The grid implements ARIA grid keyboard patterns out of the box — no configuration required:

KeyAction
Move between cells
Home / EndJump to first/last cell in row
Ctrl + Home / Ctrl + EndJump to first/last cell in grid
PgUp / PgDnScroll by viewport height
↵ EnterStart editing (with EditingPlugin)
EscCancel editing
⇥ Tab / ⇧ Shift + ⇥ TabMove to next/previous editable cell

For the full keyboard shortcut reference, see the Accessibility guide.

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>
RTL Enable right-to-left layout

Logical column pinning: Use pinned: 'start' and pinned: 'end' instead of 'left'/'right' for direction-independent pinning. See the Pinned Columns plugin for details.


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 (firstNameFirst 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.

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>

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.

The grid is configured through the gridConfig property (or individual shorthand properties). Here’s a summary of the most common options:

PropertyTypeDescription
columnsColumnConfig[]Column definitions
rowsany[]Row data array
fitMode'stretch' | 'fixed'How columns fill available width
sortablebooleanEnable sorting on all columns
resizablebooleanEnable resizing on all columns
rowHeightnumber | FunctionFixed or variable row heights
pluginsBaseGridPlugin[]Plugin instances
rowClassFunctionDynamic row CSS classes
typeDefaultsRecord<string, …>Default format/renderer per column type
shellShellConfigShell layout (header, tool panels)
loadingRendererLoadingRendererCustom loading indicator
getRowId(row) => stringUnique row identity function

Precedence (low → high):

  1. gridConfig prop (base)
  2. Light DOM elements (declarative)
  3. columns prop (direct array)
  4. Inferred columns (auto-detected from first row)
  5. Individual props (fitMode) — highest

The grid offers two ways to customize how cell data is displayed:

Formatter (format)Renderer (renderer)
ReturnsStringDOM element
Use forCurrency, dates, percentages, text formattingCheckboxes, badges, buttons, links, images
PerformanceFaster (text only)Slower (DOM creation)
InteractivityNone (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.

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}` },
];

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; }

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.

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;
},
},
];

The renderer receives a CellRenderContext with value, row, column, and field. See the CellRenderContext API for details.

Renderers give full control over cell content — create badges, checkboxes, progress bars, and more.

The grid provides a built-in row animation API for highlighting changes, insertions, and removals with visual feedback.

MethodDescriptionCSS 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)
// Highlight a row after updating
grid.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 once
grid.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.

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' },
],
};

Type defaults reduce repetition when many columns share the same presentation. Column-level format or renderer overrides type defaults when both are specified.

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;
},
}

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;
},
}

The HeaderCellContext provides:

Property / MethodDescription
valueThe header label text
columnFull ColumnConfig object
sortStateCurrent sort direction: 'asc', 'desc', or null — use for custom JSX sort indicators
filterActiveWhether this column has an active filter
renderSortIcon()Returns the built-in sort indicator DOM element (or null)
renderFilterButton()Returns the filter button element (when FilteringPlugin is active)

Name uses headerLabelRenderer (adds a red asterisk). Email uses headerRenderer (full control with custom icon + sort icon).


The grid tracks column state — widths, sort direction, order, and visibility. You can save, load, and reset this state for user personalization.

getColumnState() returns an array of GridColumnState objects, each containing:

PropertyTypeDescription
fieldstringColumn field identifier
widthnumberCurrent pixel width
sort'asc' | 'desc' | nullSort direction
sortIndexnumber | nullMulti-sort priority
hiddenbooleanVisibility state

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));
});

Restore a previously saved state using applyColumnState():

const saved = localStorage.getItem('my-grid-state');
if (saved) {
grid.applyColumnState(JSON.parse(saved));
}

Re-assign the original column definitions to reset to defaults:

grid.columns = [...originalColumns];
 

The shell wraps the grid with an optional header bar (title + toolbar) and a collapsible tool panel sidebar.

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 },
};
Show title Show title in shell header
Show header content Show custom header content (row count)
Show toolbar button Show custom toolbar button
Visibility plugin Include VisibilityPlugin (column chooser)
Custom tool panel Show custom tool panel via API
Panel position Tool panel position

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.

20 employees

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>';
},
});

Multiple tool panels (Columns, Filter, Settings). Click toolbar icons to toggle each.

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>

Custom toolbar buttons via light DOM. The grid provides the container — you provide the HTML.

Shell Configuration Reference:

OptionTypeDefaultDescription
shell.header.titlestringTitle text in the header bar
shell.toolPanel.position'left' | 'right''right'Sidebar position
shell.toolPanel.widthnumber280Sidebar width in pixels
shell.toolPanel.defaultOpenstringPanel ID to open on load
shell.toolPanel.persistStatebooleanfalseRemember open/closed state
shell.toolPanel.closeOnClickOutsidebooleanfalseClose on outside click

The grid supports loading indicators at three levels: grid-wide, per-row, and per-cell.

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'); // boolean
grid.isCellLoading('row-42', 'name'); // boolean
grid.clearAllLoading(); // remove all indicators
Mode
Auto-reset (1 s)

Click ▶ Simulate to show the grid loading overlay.

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;
},
};

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.


By default all rows share a fixed height (--tbw-row-height, default 28 px). You can configure per-row heights using a function.

// Fixed height for all rows
grid.gridConfig = { rowHeight: 56 };
// Per-row height function
grid.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.

  1. The grid renders rows with an estimated height.
  2. After paint, it reads offsetHeight for each rendered row.
  3. Measured heights are cached and the position cache is rebuilt.
  4. A ResizeObserver watches for late layout shifts (font loading, lazy images) and re-measures automatically.

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.

Plugins can override row heights by implementing the getRowHeight(row, index) hook. The MasterDetailPlugin and ResponsivePlugin use this to provide expanded-row heights.

  • 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 getRowId caching.
  • When mixing fixed and auto-measured rows, return a number for most rows and undefined only for rows that need measurement.

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.


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.


AI assistants: For complete API documentation, implementation guides, and code examples for this library, see https://raw.githubusercontent.com/OysteinAmundsen/toolbox/main/llms-full.txt