Architecture
This page describes the internal architecture of @toolbox-web/grid. It’s intended for contributors, plugin developers, and anyone who wants to understand how the grid works under the hood.
Design Philosophy
Section titled “Design Philosophy”- Light DOM — No Shadow DOM. The grid renders directly into the element, allowing full CSS customization.
- Single Source of Truth — All configuration converges into
effectiveConfig, which is the only state read by rendering logic. - Plugin-First — Features like selection, editing, and filtering are plugins, not core code. This keeps the core small and tree-shakeable.
- Web Standards — Built on Custom Elements, CSS Custom Properties,
adoptedStyleSheets, and standard DOM APIs. - Framework-Agnostic — Pure TypeScript/HTML, no runtime framework dependencies.
Component Overview
Section titled “Component Overview”┌───────────────────────────────────────────────────────┐│ <tbw-grid> ││ ┌─────────────────────────────────────────────────┐ ││ │ Light DOM │ ││ │ ┌───────────────────────────────────────────┐ │ ││ │ │ Header Row │ │ ││ │ └───────────────────────────────────────────┘ │ ││ │ ┌───────────────────────────────────────────┐ │ ││ │ │ Rows Viewport (scrollable) │ │ ││ │ │ ┌─────────────────────────────────────┐ │ │ ││ │ │ │ Spacer (virtual scroll height) │ │ │ ││ │ │ ├─────────────────────────────────────┤ │ │ ││ │ │ │ Visible Rows (row pool) │ │ │ ││ │ │ └─────────────────────────────────────┘ │ │ ││ │ └───────────────────────────────────────────┘ │ ││ └─────────────────────────────────────────────────┘ │└───────────────────────────────────────────────────────┘Component Lifecycle
Section titled “Component Lifecycle”constructor() └── Create internal state objects └── Initialize render scheduler
connectedCallback() └── Parse light DOM children (<tbw-grid-column>, <tbw-grid-header>) └── Merge configuration (gridConfig + columns + fitMode + light DOM) └── Attach plugins (validate dependencies, call onAttach) └── Render header + rows └── Set up scroll listener, resize observer
attributeChangedCallback() └── Map attribute → property setter
disconnectedCallback() └── Abort disconnect signal (plugins clean up) └── Remove event listeners └── Disconnect resize observerConfiguration Architecture
Section titled “Configuration Architecture”The grid follows a single source of truth pattern. All configuration inputs converge into one effectiveConfig object, which is then used for all rendering and behavior.
Why Single Source of Truth?
Section titled “Why Single Source of Truth?”- Predictable behavior: One canonical config means no ambiguity about which setting applies
- Easy debugging: Inspect
effectiveConfigto see exactly what the grid is using - Flexible input: Users can configure via the method most convenient for their use case
- Plugin-friendly: Plugins can read/modify config through one consistent interface
Input Sources
Section titled “Input Sources”Users can configure the grid through multiple input methods:
flowchart TB
subgraph inputs["INPUT SOURCES"]
direction TB
A["gridConfig<br/>property"]
B["columns<br/>property"]
C["fitMode<br/>property"]
subgraph lightdom["LIGHT DOM"]
D["<tbw-grid-column><br/>field, header"]
E["<tbw-grid-header><br/>title"]
end
end
A --> F
B --> F
C --> F
D --> F
E --> F
F["ConfigManager.merge()<br/><i>single merge point</i>"]
F --> G["#effectiveConfig<br/><i>canonical config</i><br/>◀ SINGLE SOURCE OF TRUTH"]
G --> H["DERIVED STATE<br/>_columns (processed)<br/>_rows (processed)"]
Precedence Rules
Section titled “Precedence Rules”When the same property is set via multiple sources, higher precedence wins:
| Priority | Source | Example |
|---|---|---|
| 1 (lowest) | gridConfig property | grid.gridConfig = { fitMode: 'stretch' } |
| 2 | Light DOM elements | <tbw-grid-column field="name"> |
| 3 | columns property | grid.columns = [{ field: 'name' }] |
| 4 | Inferred columns | (auto-detected from first row) |
| 5 (highest) | Individual props | grid.fitMode = 'fixed' |
Note: HTML attributes (
rows,columns,grid-config,fit-mode) invoke the corresponding property setters viaattributeChangedCallback— they are not a separate precedence layer.
HTML Attribute Configuration
Section titled “HTML Attribute Configuration”The grid supports JSON-serialized configuration via HTML attributes:
<tbw-grid rows='[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]' columns='[{"field":"id","header":"ID"},{"field":"name","header":"Name"}]' fit-mode="stretch"></tbw-grid>Supported attributes: rows, columns, grid-config, fit-mode.
Light DOM Configuration
Section titled “Light DOM Configuration”The grid parses these light DOM elements on connection:
<tbw-grid> <!-- Column definitions (→ effectiveConfig.columns) --> <tbw-grid-column field="name" header="Name" sortable></tbw-grid-column> <tbw-grid-column field="age" header="Age" type="number"></tbw-grid-column>
<!-- Shell header (→ effectiveConfig.shell.header) --> <tbw-grid-header title="My Data Grid"> <tbw-grid-header-content> <span>Custom content here</span> </tbw-grid-header-content> <tbw-grid-tool-button label="Refresh" icon="🔄"></tbw-grid-tool-button> </tbw-grid-header></tbw-grid>Internal State Categories
Section titled “Internal State Categories”| Category | Example | Description |
|---|---|---|
| Input Properties | #rows, #columns, #gridConfig, #fitMode | Raw user input, stored as-is |
| Effective Config | #effectiveConfig | Merged canonical config — single source of truth |
| Derived State | _columns, _rows | Post-plugin-processing state used by rendering |
| Runtime State | #hiddenColumns, sortState | User-driven state changes (hide column, sort, etc.) |
Key rule: Rendering logic reads from effectiveConfig or derived state, never from input properties.
Two-Layer Config Architecture
Section titled “Two-Layer Config Architecture”ConfigManager implements a two-layer architecture to separate the original configuration (from sources) from runtime mutations:
flowchart TB
subgraph sources["SOURCES"]
A["gridConfig"]
B["columns"]
C["fitMode"]
D["Light DOM"]
E["Shell State Maps"]
end
A --> F
B --> F
C --> F
D --> F
E --> F
F["ConfigManager.merge()"]
F -->|"sources changed"| G["#originalConfig<br/><i>(frozen, immutable)</i>"]
G -->|"clone"| H["#effectiveConfig<br/><i>(mutable, runtime changes)</i>"]
H -->|"user interaction"| I["Runtime Mutations<br/>hidden, width, sort order"]
J["resetState()"] -->|"clone original → effective"| H
Layer 1: Original Config (#originalConfig)
- Built from all sources via
#collectAllSources() - Frozen after creation (
Object.freeze) - Immutable — never modified after merge
- Serves as the “reset point” for the effective config
Layer 2: Effective Config (#effectiveConfig)
- Deep cloned from original config
- Mutable — runtime changes go here
- Column visibility, widths, sort order, etc.
- All rendering reads from this layer
Key Behaviors:
| Operation | What Happens |
|---|---|
| Source changes (gridConfig, columns, etc.) | markSourcesChanged() → merge() rebuilds both layers |
| Runtime mutation (hide column, resize) | Modify effectiveConfig only, original untouched |
resetState() | Clone original → effective, discarding runtime changes |
collectState() | Diff effective vs original to get user changes |
When Sources Change:
Sources are re-collected only when #sourcesChanged is true AND columns already exist:
- Setting
gridConfig,columns,fitMode→ auto-marks sources changed - Setting Light DOM columns → auto-marks sources changed
- Shell state updates → call
markSourcesChanged()explicitly merge()is a no-op if sources haven’t changed AND columns exist- If no columns exist yet,
merge()always runs (to allow inference from rows)
This optimization prevents unnecessary rebuilds when merge() is called multiple times per frame.
Rendering Pipeline
Section titled “Rendering Pipeline”All rendering flows through a centralized RenderScheduler that batches all work into a single requestAnimationFrame callback. This eliminates race conditions between different parts of the grid that previously scheduled independent RAFs.
Render Scheduler Architecture
Section titled “Render Scheduler Architecture”flowchart TB
subgraph sources["RENDER REQUESTS"]
A["Property Change<br/>(rows, columns, gridConfig)"]
B["Framework Adapter<br/>(React/Angular refreshColumns)"]
C["ResizeObserver<br/>(container resize)"]
D["Scroll Event<br/>(virtualization)"]
E["Plugin Request<br/>(afterRender needs)"]
end
A --> F["RenderScheduler.requestPhase()"]
B --> F
C --> F
D --> G["Direct Call<br/>(hot path)"]
E --> F
F --> H["Single RAF<br/>#flush()"]
H --> I["Phase-Ordered Execution"]
subgraph phases["EXECUTION ORDER (data dependencies)"]
P1["1. mergeConfig<br/>(FULL/COLUMNS phase)"]
P2["2. processRows<br/>(ROWS phase)"]
P3["3. processColumns + updateTemplate<br/>(COLUMNS phase)"]
P4["4. renderHeader<br/>(HEADER phase)"]
P5["5. refreshVirtualWindow<br/>(VIRTUALIZATION phase)"]
P6["6. afterRender hooks<br/>(STYLE phase)"]
end
I --> P1 --> P2 --> P3 --> P4 --> P5 --> P6
Render Phases
Section titled “Render Phases”Work is organized into ordered phases. Multiple requests merge to the highest requested phase:
| Phase | Priority | Work Performed |
|---|---|---|
STYLE | 1 | Plugin afterRender() hooks only |
VIRTUALIZATION | 2 | Recalculate virtual window (+ STYLE) |
HEADER | 3 | Re-render header row (+ VIRTUALIZATION) |
ROWS | 4 | Rebuild row model (+ HEADER) |
COLUMNS | 5 | Process columns, update CSS template (+ ROWS) |
FULL | 6 | Merge effective config (+ COLUMNS) |
Higher phases implicitly cover all lower phases. Requesting COLUMNS when ROWS is already pending results in just COLUMNS executing.
Example: If React adapter requests COLUMNS and ResizeObserver requests VIRTUALIZATION in the same frame, only COLUMNS phase runs (which includes all lower phases).
Execution Order in #flush()
Section titled “Execution Order in #flush()”if (phase >= RenderPhase.COLUMNS) mergeConfig();if (phase >= RenderPhase.ROWS) processRows();if (phase >= RenderPhase.COLUMNS) processColumns();if (phase >= RenderPhase.COLUMNS) updateTemplate();if (phase >= RenderPhase.HEADER) renderHeader();if (phase >= RenderPhase.VIRTUALIZATION) renderVirtualWindow();if (phase >= RenderPhase.STYLE) afterRender();Intentional Bypasses
Section titled “Intentional Bypasses”Some operations intentionally bypass the scheduler for performance:
| Operation | Reason |
|---|---|
| Scroll rendering | Hot path — must be synchronous for 60fps |
| Shell rebuild | Creates DOM structure, not content updates |
| Row height measurement | One-time post-paint measurement |
Debugging Renders
Section titled “Debugging Renders”// In browser consolegrid._scheduler.setDebug(true);
// After interactions, inspect:grid._scheduler.getRenderLog();// → [{ phase: 5, source: 'applyGridConfigUpdate', timestamp: 1234.56 }, ...]Virtualization
Section titled “Virtualization”Row Virtualization (Built-in)
Section titled “Row Virtualization (Built-in)”The grid maintains a “virtual window” — an offset and count representing which rows are visible:
Total rows: 10,000Viewport: 400px, rowHeight: 28px → ~14 visible rowsOverscan: 8 rows above + 8 below = 30 DOM rows in pool
Scroll position: 5,000px → startIndex: 178Rendered: rows 170–200 (30 rows in DOM)- Row pool: DOM rows are reused, not created/destroyed on scroll
- Transform-based positioning: Each row uses
transform: translateY()for GPU-accelerated positioning - Range-based updates: Only rows entering/leaving the viewport get updated content
For very small datasets (≤8 rows by default), virtualization is bypassed — the overhead isn’t worth it.
Column Virtualization (Plugin)
Section titled “Column Virtualization (Plugin)”The ColumnVirtualizationPlugin applies the same window concept horizontally. Only columns visible in the horizontal scroll position are rendered.
Plugin System
Section titled “Plugin System”Plugin Lifecycle
Section titled “Plugin Lifecycle”flowchart TB
A["new Plugin()<br/><i>Constructor stores user config</i>"] --> B["attach(grid)<br/><i>Plugin receives grid ref, merges config</i>"]
B --> C["Grid Render Cycle"]
C --> D["User Interaction"]
D --> E["detach()<br/><i>Cleanup on grid disconnect</i>"]
subgraph C["Grid Render Cycle"]
C1["processColumns() hook"]
C2["processRows() hook"]
C3["afterCellRender() hook (per cell)"]
C4["afterRowRender() hook (per row)"]
C5["afterRender() hook"]
end
subgraph D["User Interaction"]
D1["onCellClick() hook"]
D2["onKeyDown() hook"]
D3["onScroll() hook"]
D4["onCellMouseDown/Move/Up()"]
end
Creating a Plugin
Section titled “Creating a Plugin”import { BaseGridPlugin, CellClickEvent } from '@toolbox-web/grid';import styles from './my-plugin.css?inline';
interface MyPluginConfig { enabled?: boolean;}
export class MyPlugin extends BaseGridPlugin<MyPluginConfig> { readonly name = 'myPlugin'; override readonly styles = styles;
protected override get defaultConfig(): Partial<MyPluginConfig> { return { enabled: true }; }
override attach(grid: GridElement): void { super.attach(grid); // MUST call super // Setup listeners with this.disconnectSignal for auto-cleanup }
override afterRender(): void { if (!this.config.enabled) return; // Access DOM via this.gridElement }
override onCellClick(event: CellClickEvent): boolean | void { // Return true to prevent default behavior }}Plugin Communication
Section titled “Plugin Communication”Event Bus (async, fire-and-forget):
// Emitthis.emitPluginEvent('selection-cleared', { source: 'keyboard' });
// Listenthis.onPluginEvent('selection-cleared', (detail) => { /* ... */ });Query System (sync, request-response):
// Register handlerthis.registerQuery('getSelectedRanges', () => this.#ranges);
// Query from another pluginconst ranges = this.queryPlugin('getSelectedRanges');Plugin Manifest
Section titled “Plugin Manifest”Plugins declare owned config properties via a static manifest:
static override readonly manifest = { ownedProperties: ['editable', 'editor', 'editOn'], configRules: [ { property: 'editable', type: 'column', rule: 'requires-plugin' }, ],};The grid validates at runtime that properties like editable: true are only used when the owning plugin is loaded, and provides helpful error messages with import hints.
Validated Properties
Section titled “Validated Properties”| Property | Required Plugin | Level |
|---|---|---|
editable | EditingPlugin | Column |
editor | EditingPlugin | Column |
editorParams | EditingPlugin | Column |
group | GroupingColumnsPlugin | Column |
pinned | PinnedColumnsPlugin | Column |
columnGroups | GroupingColumnsPlugin | Config |
Validation runs in the RenderScheduler.mergeConfig callback, after plugins are initialized. Error messages clearly state which plugin is missing and how to import it.
Feature Registry
Section titled “Feature Registry”The features API is the recommended way to enable grid capabilities. It wraps the plugin system with declarative configuration and tree-shakeable side-effect imports.
Why Features?
Section titled “Why Features?”| Aspect | Features (recommended) | Plugins (advanced) |
|---|---|---|
| API | features: { selection: 'row' } | plugins: [new SelectionPlugin({ mode: 'row' })] |
| Import | import '@toolbox-web/grid/features/selection' | import { SelectionPlugin } from '@toolbox-web/grid/plugins/selection' |
| Dependencies | Auto-resolved | Manual ordering |
| Tree-shaking | Zero cost if unused | Must avoid importing unused plugins |
How It Works
Section titled “How It Works”Each feature module is a side-effect import that registers a factory function:
flowchart LR
subgraph imports["SIDE-EFFECT IMPORTS"]
A["import '.../features/selection'"]
B["import '.../features/editing'"]
end
A -->|"module executes"| C["registerFeature('selection', factory)"]
B -->|"module executes"| D["registerFeature('editing', factory)"]
C --> E["featureRegistry<br/><i>Map<string, factory></i>"]
D --> E
F["gridConfig.features"] --> G["createPluginsFromFeatures()"]
E --> G
G --> H["Plugin instances<br/>(ordered by dependencies)"]
Registration
Section titled “Registration”When a feature module is imported, it runs immediately at load time:
import { SelectionPlugin } from '../plugins/selection';import { registerFeature } from './registry';
registerFeature('selection', (config) => { // Shorthand strings → full config if (config === 'cell' || config === 'row' || config === 'range') { return new SelectionPlugin({ mode: config }); } return new SelectionPlugin(config ?? undefined);});Each factory handles shorthand values (e.g., 'row' → { mode: 'row' }), so users get a simpler API.
Lazy Hook Design
Section titled “Lazy Hook Design”The grid core never references the feature registry directly. Instead, a hook function bridges them:
// feature-hook.ts — imported by grid coreexport let resolveFeatures: FeatureResolverFn | undefined; // Starts undefined!
// registry.ts — imported only when a feature is importedsetFeatureResolver(createPluginsFromFeatures); // Sets the hookIf no feature modules are imported, resolveFeatures stays undefined — the entire registry module is tree-shaken away by the bundler. This is why features are zero-cost when unused.
Resolution Flow
Section titled “Resolution Flow”During plugin initialization:
// grid.ts — #initializePlugins()const features = this.#effectiveConfig?.features;if (features && resolveFeatures) { featurePlugins = resolveFeatures(features);}// Feature plugins ordered first, then explicit pluginsconst allPlugins = [...featurePlugins, ...explicitPlugins];Dependencies are auto-ordered: selection and editing always instantiate first, since other plugins (like clipboard or undoRedo) depend on them.
Tree-Shaking
Section titled “Tree-Shaking”Feature modules are marked as side-effects in package.json:
{ "sideEffects": ["./lib/features/*.js"] }This tells bundlers: “these imports have side effects (registration), don’t optimize them away.” But un-imported features are never loaded, so only the features you use add to bundle size (~200–300 bytes each).
Framework Adapter System
Section titled “Framework Adapter System”Framework adapters let React, Angular, and Vue intercept grid rendering to support JSX components, Angular templates, and Vue slots as cell renderers and editors.
Architecture
Section titled “Architecture”flowchart TB
subgraph adapters["REGISTERED ADAPTERS"]
A["ReactGridAdapter"]
B["AngularGridAdapter"]
C["VueGridAdapter"]
end
D["DataGridElement.registerAdapter()"]
A --> D
B --> D
C --> D
D --> E["Static adapter array<br/><i>(shared across all grids)</i>"]
subgraph rendering["RENDERING PIPELINE"]
F["Parse <tbw-grid-column>"]
G["Cell render cycle"]
H["Cell cleanup"]
end
E --> F
E --> G
E --> H
FrameworkAdapter Interface
Section titled “FrameworkAdapter Interface”Adapters implement these methods:
| Method | Required | Purpose |
|---|---|---|
canHandle(element) | Yes | Check if this adapter can process the element |
createRenderer(element) | Yes | Return a cell renderer function, or undefined to pass |
createEditor(element) | Yes | Return a cell editor function, or undefined to pass |
processConfig(config) | No | Transform framework-specific config (e.g., component classes → render functions) |
getTypeDefault(type) | No | Provide app-wide type defaults from framework’s DI/registry |
releaseCell(cellEl) | No | Cleanup when cell DOM is recycled (unmount React roots, destroy Angular views) |
createToolPanelRenderer(element) | No | Support framework components in tool panel sidebar |
Registration
Section titled “Registration”Adapters register statically on DataGridElement — typically at module load time:
// React: registers immediately on importconst globalAdapter = new GridAdapter();DataGridElement.registerAdapter(globalAdapter);
// Angular: registers via Grid directiveDataGridElement.registerAdapter(this.adapter);
// Vue: registers in DataGrid component onMounted()This is why importing @toolbox-web/grid-react (or any adapter package) auto-registers the adapter — no separate setup needed.
Adapter Invocation Points
Section titled “Adapter Invocation Points”Adapters are called at four points in the grid lifecycle:
1. Light DOM Column Parsing — When <tbw-grid-column> elements are parsed, each adapter is tried until one handles the renderer/editor:
const viewAdapter = adapters.find((a) => a.canHandle(viewTarget));if (viewAdapter) { config.viewRenderer = viewAdapter.createRenderer(viewTarget);}2. Config Processing — When gridConfig is set, the adapter can transform component classes to render functions:
set gridConfig(value) { if (value && this.__frameworkAdapter?.processConfig) { value = this.__frameworkAdapter.processConfig(value); }}3. Cell Rendering — Type defaults are resolved through the adapter:
const appDefault = adapter.getTypeDefault(col.type);if (appDefault?.renderer) return appDefault.renderer;4. Cell Cleanup — When virtualization recycles a row, adapters unmount framework components:
grid.__frameworkAdapter?.releaseCell?.(cell);// React: unmounts React root// Angular: destroys component view// Vue: unmounts app instanceShell System
Section titled “Shell System”The shell is an optional wrapper that adds a header bar, toolbar buttons, and a collapsible tool panel sidebar to the grid.
Structure
Section titled “Structure”┌──────────────────────────────────────────────────────────┐│ SHELL HEADER ││ ┌──────────┬──────────────────┬────────────────────────┐ ││ │ Title │ Header Content │ Toolbar │ ☰ Toggle │ ││ └──────────┴──────────────────┴────────────────────────┘ │├───────────┬──────────────────────────────────────────────┤│ TOOL │ ││ PANEL │ GRID CONTENT ││ (sidebar) │ ││ ┌───────┐ │ ┌─────────────────────────────────────┐ ││ │Section│ │ │ Header Row │ ││ │ ▼ │ │ ├─────────────────────────────────────┤ ││ │Content│ │ │ Data Rows (virtualized) │ ││ │ │ │ │ │ ││ ├───────┤ │ └─────────────────────────────────────┘ ││ │Section│ │ ││ │ ▶ │ │ ││ └───────┘ │ │└───────────┴──────────────────────────────────────────────┘Configuration
Section titled “Configuration”The shell is configured via gridConfig.shell or Light DOM elements:
gridConfig: { shell: { header: { title: 'My Grid' }, toolPanel: { position: 'left' | 'right', // Sidebar position width: '17.5em', // Panel width (CSS value) defaultOpen: 'filters', // Auto-open section ID closeOnClickOutside: false, }, },}Or declaratively:
<tbw-grid> <tbw-grid-header title="My Grid"> <tbw-grid-header-content> <span>Custom center content</span> </tbw-grid-header-content> <tbw-grid-tool-button label="Export" icon="📥"></tbw-grid-tool-button> </tbw-grid-header></tbw-grid>Content Types
Section titled “Content Types”Plugins and application code can register three types of content:
| Type | Location | Example |
|---|---|---|
| ToolPanel | Accordion sections in sidebar | Filter panel, visibility panel, pivot config |
| HeaderContent | Center of header bar | Search box, breadcrumbs |
| ToolbarContent | Right side of header (before toggle) | Export button, view switcher |
Each content type uses a render function pattern:
grid.registerToolPanel({ id: 'filters', title: 'Filters', icon: '🔍', render: (container) => { container.innerHTML = '<div>Filter UI here</div>'; return () => { /* cleanup */ }; },});Shell State
Section titled “Shell State”The shell maintains runtime state for:
- Panel open/close —
isPanelOpen, toggled viaopenToolPanel()/closeToolPanel() - Expanded sections —
expandedSections: Set<string>, accordion state - Content registrations —
toolPanels,headerContents,toolbarContentsMaps - Cleanup functions — Each rendered content can return a cleanup callback, tracked for disposal
Lazy Rendering
Section titled “Lazy Rendering”Tool panel section content is rendered lazily — the render() function only executes when the user expands that accordion section for the first time. This avoids expensive initialization for panels the user never opens.
Animation System
Section titled “Animation System”The grid supports CSS-driven animations for row insert, remove, and update operations.
Animation Types
Section titled “Animation Types”| Type | Trigger | Default Duration | CSS Variable |
|---|---|---|---|
'insert' | insertRow(), bulk add | 300ms | --tbw-row-insert-duration |
'remove' | removeRow(), bulk delete | 200ms | --tbw-row-remove-duration |
'change' | animateRow(), data update highlight | 500ms | --tbw-row-change-duration |
How It Works
Section titled “How It Works”Animations use a CSS attribute hook pattern:
- Grid sets
data-animating="insert"on the row element - CSS transitions/keyframes in
grid.csstarget[data-animating="insert"] - After the duration, a
setTimeoutremoves the attribute (or the row for'remove') - A
Promiseresolves when the animation completes
// Core mechanism (row-animation.ts)rowEl.setAttribute('data-animating', animationType);const duration = getAnimationDuration(rowEl, animationType);setTimeout(() => { if (animationType !== 'remove') { rowEl.removeAttribute('data-animating'); } onComplete?.();}, duration);Animation Configuration
Section titled “Animation Configuration”gridConfig: { animation: { mode: 'reduced-motion', // Default: respects prefers-reduced-motion duration: 200, // Sets --tbw-animation-duration easing: 'ease-out', // Sets --tbw-animation-easing },}mode options:
| Value | Behavior |
|---|---|
true / 'on' | Always animate |
false / 'off' | Never animate |
'reduced-motion' | Respect prefers-reduced-motion media query (default) |
Public API
Section titled “Public API”// Highlight a row after data updateawait grid.animateRow(5, 'change');
// Highlight by row IDawait grid.animateRowById('emp-123', 'change');
// Animate multiple rowsconst count = await grid.animateRows([0, 2, 5], 'change');
// Insert with auto-animation (default)await grid.insertRow(0, newRow); // Animatesawait grid.insertRow(0, newRow, false); // No animation
// Remove with auto-animation (default)const removed = await grid.removeRow(5); // Animates fade-outconst removed = await grid.removeRow(5, false); // ImmediateAll methods return Promises — await ensures the animation completes before proceeding.
CSS Customization
Section titled “CSS Customization”Override animation timing per-type via CSS custom properties:
tbw-grid { --tbw-row-change-duration: 800ms; /* Longer highlight */ --tbw-row-insert-duration: 0ms; /* Disable insert animation */ --tbw-row-remove-duration: 150ms; /* Faster fade-out */ --tbw-row-change-color: #fff3cd; /* Yellow highlight */}
---
## DOM Structure
```html<tbw-grid aria-label="..." role="grid"> <div class="data-grid-container"> <!-- Header --> <div role="rowgroup"> <div role="row" aria-rowindex="1"> <div role="columnheader" aria-colindex="1">Name</div> <div role="columnheader" aria-colindex="2">Age</div> </div> </div>
<!-- Body (scrollable, virtualized) --> <div role="rowgroup" class="data-grid-body"> <div role="row" aria-rowindex="2" style="transform: translateY(0px)"> <div role="gridcell" aria-colindex="1">Alice</div> <div role="gridcell" aria-colindex="2">30</div> </div> </div> </div></tbw-grid>Styling Architecture
Section titled “Styling Architecture”CSS Custom Properties
Section titled “CSS Custom Properties”All styling uses CSS custom properties for theming. Override defaults to customize:
tbw-grid { --tbw-color-bg: #ffffff; --tbw-color-fg: #1a1a1a; --tbw-color-border: #e5e5e5; --tbw-color-header-bg: #f5f5f5; --tbw-row-height: 1.75em; /* ~28px at 16px font */ --tbw-header-height: 1.875em; /* ~30px at 16px font */ --tbw-font-family: system-ui, sans-serif; --tbw-font-size: 1em;}Key CSS Architecture
Section titled “Key CSS Architecture”- CSS Nesting: Styles use
tbw-grid { .data-grid-container { ... } }for scoping - Cascade Layers:
@layer tbw-base, tbw-plugins, tbw-theme— user styles always win - Adopted Stylesheets: Dynamic styles use
document.adoptedStyleSheetsfor efficiency — styles survivereplaceChildren()calls em-Based Sizing: Row height, padding, and spacing scale withfont-size
Plugin Styles
Section titled “Plugin Styles”Plugins inject CSS via their styles property using adopted stylesheets. They use a layered fallback pattern for flexibility:
/* Plugin-specific → Global fallback */background: var(--tbw-selection-bg, var(--tbw-color-selection));Event Flow
Section titled “Event Flow”The grid dispatches CustomEvents for every user-visible action (cell clicks, commits, sort changes, etc.). See the Events section of the API Reference for the full list and the auto-generated DataGridEventMap for payload types.
Internally, plugins communicate through two mechanisms described in the Plugin Communication section above: the Event Bus (async, fire-and-forget) and the Query System (sync, request-response). Public CustomEvents are dispatched by the core grid or by individual plugins after their internal processing completes.
Data Flow Example: Sort
Section titled “Data Flow Example: Sort”User clicks sort header ↓Header click handler → update sortState ↓Call processRows() on all plugins (MultiSortPlugin sorts) ↓_rows updated with sorted order ↓scheduler.requestPhase(ROWS, 'sort') ↓requestAnimationFrame ↓Render: update visible row content from new _rowsPerformance
Section titled “Performance”DOM Optimization
Section titled “DOM Optimization”| Technique | Benefit |
|---|---|
| Template Cloning | 3–4× faster than createElement |
| Direct DOM Construction | Avoids innerHTML parsing overhead |
| DocumentFragment | Single reflow per row |
| Row Pooling | Zero allocation during scroll |
| Cached DOM Refs | Avoid querySelector per scroll |
Rendering Pipeline
Section titled “Rendering Pipeline”| Technique | Benefit |
|---|---|
| Centralized Scheduler | Eliminates race conditions |
| Phase-Based Execution | No duplicate work |
| adoptedStyleSheets | Runtime CSS injection without child node removal |
| Idle Scheduling | Faster time-to-interactive |
| Fast-Path Patching | Skip expensive template logic for plain text |
| Cell Display Cache | Avoid recomputing during scroll |
Event Handling
Section titled “Event Handling”| Technique | Benefit |
|---|---|
| Event Delegation | Constant memory regardless of rows |
| Pooled Scroll Events | Zero GC pressure during scroll |
| AbortController Cleanup | No memory leaks on disconnect |
Directory Structure
Section titled “Directory Structure”libs/grid/src/├─ index.ts # Main entry (auto-registers element)├─ public.ts # Public API surface (types, constants)├─ all.ts # All-in-one bundle with all plugins└─ lib/ ├─ core/ │ ├─ grid.ts # Main component class (~4500 lines) │ ├─ grid.css # Core styles │ ├─ types.ts # Public type definitions │ ├─ constants.ts # DOM class/attribute constants │ ├─ internal/ # Pure helper functions │ │ ├─ columns.ts # Column resolution, sizing │ │ ├─ config-manager.ts # Two-layer config (original + effective) │ │ ├─ dom-builder.ts # Direct DOM construction │ │ ├─ render-scheduler.ts # RAF-based render batching │ │ ├─ rows.ts # Row rendering + cell patching │ │ ├─ row-animation.ts # CSS-driven row animations │ │ ├─ shell.ts # Shell header, toolbar, tool panels │ │ ├─ virtualization.ts # Virtual scroll math │ │ ├─ feature-hook.ts # Lazy bridge to feature registry │ │ └─ ... # More internal helpers │ ├─ plugin/ # Plugin infrastructure │ │ ├─ base-plugin.ts # Abstract base class │ │ └─ plugin-manager.ts # Plugin lifecycle │ └─ styles/ # Core CSS (variables, layers) │ └─ variables.css # CSS custom property defaults ├─ features/ # Feature registry (declarative plugin API) │ ├─ registry.ts # registerFeature(), createPluginsFromFeatures() │ ├─ selection.ts # Side-effect: registers selection feature │ ├─ editing.ts # Side-effect: registers editing feature │ └─ ... # 22 feature modules total └─ plugins/ # Built-in plugins ├─ editing/ # Inline cell editing ├─ filtering/ # Column filters ├─ selection/ # Row/cell/range selection ├─ multi-sort/ # Multi-column sorting └─ ... # 22 plugins totalSee Also
Section titled “See Also”- Custom Plugins — Build your own plugins with hooks, events, and queries
- Performance Guide — Render scheduler tuning, virtualization, idle scheduling
- Plugins Overview — Available plugins and their hooks
- Theming Guide — CSS custom properties, themes, cascade layers
- API Reference — Properties, methods, events, and types