Skip to content

How to use icons in cells

Render an icon from the icon pack inside a cell, and wire up an icon-only control so it stays accessible to screen reader users.

Prerequisites

Steps

  1. Install the icon pack.

    Terminal window
    npm install @handsontable/spreadsheet-icons

    You can also copy a single icon’s SVG markup directly from the icon pack page — click any icon in the catalog to copy its name, then look up its markup in the downloaded package.

  2. Render the icon inside a custom renderer.

    Build the icon markup once, then swap it into the cell based on the cell’s value. This example renders a “flag” icon that toggles between a filled and an empty state.

  3. Add an accessible name to the control, not the icon.

    The icon itself — an inline <svg> — has no accessible name. If you wrap it in a clickable element (a <button>, or a <div> with role="button"), set aria-label on that element instead, and update it whenever the icon’s state changes.

JavaScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// Register all Handsontable's modules.
registerAllModules();
const data = [
{ task: 'Update API docs', assignee: 'Ana García', dueDate: '2025-06-30', flagged: true },
{ task: 'Deploy hotfix', assignee: 'James Okafor', dueDate: '2025-06-18', flagged: false },
{ task: 'Review pull request', assignee: 'Li Wei', dueDate: '2025-06-20', flagged: false },
{ task: 'Write release notes', assignee: 'Sara Nowak', dueDate: '2025-06-25', flagged: true },
{ task: 'Fix flaky test', assignee: 'Marco Rossi', dueDate: '2025-06-22', flagged: false },
];
// Icons from the `@handsontable/spreadsheet-icons` package - see the "Icon
// pack" reference page for the full set.
// `currentColor` picks up the button's text color, so the icon stays visible
// in both light and dark themes instead of a hardcoded color disappearing
// against the background.
const FLAG_FULL_ICON = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M6,4.8H5V20H6V13.21c4.18,2.51,8.37-2.51,12.56,0V4.8C14.38,2.29,10.19,7.31,6,4.8Z"/></svg>';
const FLAG_EMPTY_ICON = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="currentColor" opacity="0.6"><path d="M15.87,4.08c-2.41,0-4.83,1.45-7.25,1.45A5,5,0,0,1,6,4.81v0H5V20H6V13.29A5,5,0,0,0,8.62,14c2.42,0,4.84-1.45,7.25-1.45a5,5,0,0,1,2.66.72V4.8A5,5,0,0,0,15.87,4.08Zm1.66,7.68a6.45,6.45,0,0,0-1.66-.21,13.64,13.64,0,0,0-3.91.77A12.11,12.11,0,0,1,8.62,13,4.17,4.17,0,0,1,7,12.67l-1-.43V5.9l1,.41a6,6,0,0,0,1.66.22,13.51,13.51,0,0,0,3.91-.77,12.11,12.11,0,0,1,3.34-.68,4,4,0,0,1,1.66.33Z"/></svg>';
const ExampleComponent = () => {
// The icon itself has no accessible name, so the button carries an
// `aria-label` that describes what clicking it does.
const flagRenderer = (instance, td, row, _col, _prop, value) => {
const taskName = String(instance.getDataAtCell(row, 0));
const button = document.createElement('button');
button.type = 'button';
button.style.cssText = 'background: none; border: none; cursor: pointer; padding: 0.25rem;';
button.innerHTML = value ? FLAG_FULL_ICON : FLAG_EMPTY_ICON;
button.setAttribute('aria-label', value ? `Remove the follow-up flag from ${taskName}` : `Flag ${taskName} for follow-up`);
button.addEventListener('click', () => {
instance.setDataAtCell(row, 3, !value);
});
td.innerText = '';
td.appendChild(button);
return td;
};
return (<HotTable data={data} colHeaders={['Task', 'Assignee', 'Due date', 'Flagged']} columns={[
{ data: 'task' },
{ data: 'assignee' },
{ data: 'dueDate', type: 'date', dateFormat: { year: 'numeric', month: '2-digit', day: '2-digit' } },
{ data: 'flagged', renderer: flagRenderer, className: 'htCenter' },
]} height="auto" autoWrapRow={true} autoWrapCol={true} licenseKey="non-commercial-and-evaluation"/>);
};
export default ExampleComponent;
TypeScript
import { HotTable } from '@handsontable/react-wrapper';
import Handsontable from 'handsontable/base';
import { registerAllModules } from 'handsontable/registry';
import type { BaseRenderer } from 'handsontable/renderers';
// Register all Handsontable's modules.
registerAllModules();
interface Task {
task: string;
assignee: string;
dueDate: string;
flagged: boolean;
}
const data: Task[] = [
{ task: 'Update API docs', assignee: 'Ana García', dueDate: '2025-06-30', flagged: true },
{ task: 'Deploy hotfix', assignee: 'James Okafor', dueDate: '2025-06-18', flagged: false },
{ task: 'Review pull request', assignee: 'Li Wei', dueDate: '2025-06-20', flagged: false },
{ task: 'Write release notes', assignee: 'Sara Nowak', dueDate: '2025-06-25', flagged: true },
{ task: 'Fix flaky test', assignee: 'Marco Rossi', dueDate: '2025-06-22', flagged: false },
];
// Icons from the `@handsontable/spreadsheet-icons` package - see the "Icon
// pack" reference page for the full set.
// `currentColor` picks up the button's text color, so the icon stays visible
// in both light and dark themes instead of a hardcoded color disappearing
// against the background.
const FLAG_FULL_ICON =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M6,4.8H5V20H6V13.21c4.18,2.51,8.37-2.51,12.56,0V4.8C14.38,2.29,10.19,7.31,6,4.8Z"/></svg>';
const FLAG_EMPTY_ICON =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="currentColor" opacity="0.6"><path d="M15.87,4.08c-2.41,0-4.83,1.45-7.25,1.45A5,5,0,0,1,6,4.81v0H5V20H6V13.29A5,5,0,0,0,8.62,14c2.42,0,4.84-1.45,7.25-1.45a5,5,0,0,1,2.66.72V4.8A5,5,0,0,0,15.87,4.08Zm1.66,7.68a6.45,6.45,0,0,0-1.66-.21,13.64,13.64,0,0,0-3.91.77A12.11,12.11,0,0,1,8.62,13,4.17,4.17,0,0,1,7,12.67l-1-.43V5.9l1,.41a6,6,0,0,0,1.66.22,13.51,13.51,0,0,0,3.91-.77,12.11,12.11,0,0,1,3.34-.68,4,4,0,0,1,1.66.33Z"/></svg>';
const ExampleComponent = () => {
// The icon itself has no accessible name, so the button carries an
// `aria-label` that describes what clicking it does.
const flagRenderer: BaseRenderer = (
instance: Handsontable,
td: HTMLTableCellElement,
row: number,
_col: number,
_prop: string | number,
value: Handsontable.CellValue
) => {
const taskName = String(instance.getDataAtCell(row, 0));
const button = document.createElement('button');
button.type = 'button';
button.style.cssText = 'background: none; border: none; cursor: pointer; padding: 0.25rem;';
button.innerHTML = value ? FLAG_FULL_ICON : FLAG_EMPTY_ICON;
button.setAttribute(
'aria-label',
value ? `Remove the follow-up flag from ${taskName}` : `Flag ${taskName} for follow-up`
);
button.addEventListener('click', () => {
instance.setDataAtCell(row, 3, !value);
});
td.innerText = '';
td.appendChild(button);
return td;
};
return (
<HotTable
data={data}
colHeaders={['Task', 'Assignee', 'Due date', 'Flagged']}
columns={[
{ data: 'task' },
{ data: 'assignee' },
{ data: 'dueDate', type: 'date', dateFormat: { year: 'numeric', month: '2-digit', day: '2-digit' } },
{ data: 'flagged', renderer: flagRenderer, className: 'htCenter' },
]}
height="auto"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;

Click a flag icon to toggle it. Each click updates both the underlying cell value (through setDataAtCell()) and the button’s aria-label, so the accessible name always matches what the icon shows.

Result

You have a cell that renders an icon based on its data, with a click handler that updates the grid, and an accessible name that a screen reader announces correctly in both icon states.