Handsontable Renderer Development
SkillDev toolsUse when creating or modifying a Handsontable cell renderer function that controls how cell content is displayed in the DOM - pure functions that take cell data and modify TD element
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Handsontable Renderer Development skill
What this skill tells your AI
The instructions your AI receives, as published by handsontable/handsontable in .claude/skills/handsontable-renderer-dev/SKILL.md and read by ahel’s review.
Function signature
Renderers are pure functions with no class or state:
function myRenderer(hotInstance, TD, row, col, prop, value, cellProperties) {
baseRenderer.apply(this, arguments);
// Modify TD element here
}
Always call baseRenderer first. It applies common properties: readonly CSS class, invalid CSS class, ARIA attributes, and other standard cell setup.
Key rules
- Stateless and read-only. Renderers only modify the TD element's DOM content and attributes. Never store state, attach event listeners, or mutate data.
- Use
fastInnerText(TD, value)fromsrc/helpers/dom/element.tsfor setting cell text content. It is XSS-safe and cross-browser optimized. - Append or clear through
getCellContentRoot(TD), neverTDitself, when a renderer manages the cell's children (empty(...),appendChild(...),insertBefore(x, root.firstChild)). A row rendered at an exact height keeps the content in adiv.htCellClipwrapper (a table cell cannot be shorter than its in-flow content); the helper returns that wrapper when present and the cell otherwise.fastInnerText/fastInnerHTMLdo this on their own. Models:checkboxRenderer,autocompleteRenderer,multiSelectRenderer. - Never use
innerHTMLwithout sanitization. All user-provided content must be escaped to prevent XSS. - No event listeners. If you need interactivity, that belongs in an editor or a plugin, not a renderer.
- ARIA attributes.
baseRendererhandles standard ARIA. If your renderer changes the cell's role or state, update ARIA attributes accordingly.
File structure
src/renderers/{rendererName}/
{rendererName}.ts # Renderer function
index.ts # Re-exports
Registry: src/renderers/registry.ts.
Registration
import { registerRenderer } from '../../renderers/registry';
registerRenderer('myRenderer', myRenderer);
Reference implementations
src/renderers/baseRenderer/baseRenderer.ts- Must be called by every renderer.src/renderers/textRenderer/textRenderer.ts- Simplest renderer, good starting template.src/renderers/htmlRenderer/htmlRenderer.ts- Renders raw HTML (use with caution).src/renderers/numericRenderer/numericRenderer.ts- Formatting with numeral.js.
Performance
Renderers are called for every cell in the viewport on every render cycle (both fast and slow renders). They must be highly optimized:
- Keep logic minimal - avoid DOM-heavy operations
- Never read layout properties inside a renderer (
getBoundingClientRect,offsetWidth) - causes layout thrashing - Avoid object allocations and complex string concatenations in the hot path
- The simpler the renderer, the better
- A slow derived value (chart markup, a parsed document) belongs in a cache keyed by the data record or the cell coordinates, never by the
TD: the engine keeps a fixed set ofTDelements and rewrites them as you scroll, so aTD-keyed cache misses on almost every call (issue #13446). Two traps when building that cache:getSourceDataAtRow()returns a copy of the row on every call, so it can never be aWeakMapkey - read the record from your own data array bytoPhysicalRow(row)(the cellvalueitself IS handed over by reference); andafterRenderdoes not fire for scroll draws - count or refresh per-draw state inafterViewRender. Worked example:docs/content/recipes/performance/expensive-cell-renderer/.
Common mistakes
- Forgetting to call
baseRendererfirst, which skips readonly/invalid CSS and ARIA setup. - Caching renderer output on the
TDelement (aWeakMapkeyed byTD, or a property on it) - see Performance above. - Adding event listeners in a renderer (use editors or plugins instead).
- Using
innerHTMLwith unsanitized user input. - Writing children straight into
TD(TD.appendChild,TD.insertBefore(x, TD.firstChild),empty(TD)) instead ofgetCellContentRoot(TD)— on an exact-height row the clipping wrapper is then rebuilt every draw, and a node left outside it grows the row back. - Mutating
cellPropertiesor source data inside a renderer. - Not handling
nullorundefinedvalues gracefully.
Signals
- GitHub stars
- 22k
- Forks
- 3k
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
handsontable-renderer-dev- Source
- github.com/handsontable/handsontable