Skip to content

Comments

Add a comment (a note) to a cell, using the context menu, just like in Excel. Edit and delete comments. Make comments read-only.

The Comments plugin lets users attach text notes to individual cells. Use it when reviewers need to annotate data without changing cell values.

Enable the plugin

Set the comments configuration option to true to enable the feature and add all the needed context menu items. For example:

<HotTable
data={[
['Update API docs', 'Ana García', 'In progress'],
['Deploy hotfix', 'James Okafor', 'Blocked'],
]}
comments={true}
/>

Add comments via the context menu

After you’ve enabled the plugin, the Context Menu gains a few new items:

  • Add/Edit comment
  • Delete comment
  • Read-only comment

Set up pre-set comments

You can also pre-define comments for your table. Comments are stored in the table’s/column’s/cell’s metadata object and you can declare as any value of the respective type. For example:

cell={[
{ row: 1, col: 1, comment: { value: 'Hello world!' } }
]}

In this example, the comment “Hello world!” is added to the cell at (1,1).

Basic example

JavaScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
return (
<HotTable
data={[
['', 'Tesla', 'Nissan', 'Toyota', 'Honda', 'Mazda', 'Ford'],
['2017', 10, 11, 12, 13, 15, 16],
['2018', 10, 11, 12, 13, 15, 16],
['2019', 10, 11, 12, 13, 15, 16],
['2020', 10, 11, 12, 13, 15, 16],
['2021', 10, 11, 12, 13, 15, 16],
]}
rowHeaders={true}
colHeaders={true}
contextMenu={true}
comments={true}
cell={[
{ row: 1, col: 1, comment: { value: 'Some comment' } },
{ row: 2, col: 2, comment: { value: 'More comments' } },
]}
height="auto"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;
TypeScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
return (
<HotTable
data={[
['', 'Tesla', 'Nissan', 'Toyota', 'Honda', 'Mazda', 'Ford'],
['2017', 10, 11, 12, 13, 15, 16],
['2018', 10, 11, 12, 13, 15, 16],
['2019', 10, 11, 12, 13, 15, 16],
['2020', 10, 11, 12, 13, 15, 16],
['2021', 10, 11, 12, 13, 15, 16],
]}
rowHeaders={true}
colHeaders={true}
contextMenu={true}
comments={true}
cell={[
{ row: 1, col: 1, comment: { value: 'Some comment' } },
{ row: 2, col: 2, comment: { value: 'More comments' } },
]}
height="auto"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;

Make a comment read-only

By default, all comments are editable. To change this, set the readOnly configuration option to true when adding a comment. This example makes the “Tesla” comment attached to a cell read-only, whereas the “Honda” comment attached to another cell is editable.

JavaScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
return (
<HotTable
data={[
['', 'Tesla', 'Toyota', 'Honda', 'Ford'],
['2018', 10, 11, 12, 13, 15, 16],
['2019', 10, 11, 12, 13, 15, 16],
['2020', 10, 11, 12, 13, 15, 16],
]}
rowHeaders={true}
colHeaders={true}
contextMenu={true}
comments={true}
cell={[
{
row: 0,
col: 1,
comment: { value: 'A read-only comment.', readOnly: true },
},
{ row: 0, col: 3, comment: { value: 'You can edit this comment' } },
]}
height="auto"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;
TypeScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
return (
<HotTable
data={[
['', 'Tesla', 'Toyota', 'Honda', 'Ford'],
['2018', 10, 11, 12, 13, 15, 16],
['2019', 10, 11, 12, 13, 15, 16],
['2020', 10, 11, 12, 13, 15, 16],
]}
rowHeaders={true}
colHeaders={true}
contextMenu={true}
comments={true}
cell={[
{
row: 0,
col: 1,
comment: { value: 'A read-only comment.', readOnly: true },
},
{ row: 0, col: 3, comment: { value: 'You can edit this comment' } },
]}
height="auto"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;

Set a comment box’s size

To set the width and height of a comment box, use the style parameter.

JavaScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
return (
<HotTable
data={[
['', 'Tesla', 'Nissan', 'Toyota', 'Honda', 'Mazda', 'Ford'],
['2017', 10, 11, 12, 13, 15, 16],
['2018', 10, 11, 12, 13, 15, 16],
['2019', 10, 11, 12, 13, 15, 16],
]}
rowHeaders={true}
colHeaders={true}
contextMenu={true}
comments={true}
cell={[
{ row: 1, col: 1, comment: { value: 'Some comment' } },
// add the `style` parameter
{
row: 2,
col: 2,
comment: {
value: 'Comment 200x50 px',
style: { width: 200, height: 50 },
},
},
]}
height="auto"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;
TypeScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
return (
<HotTable
data={[
['', 'Tesla', 'Nissan', 'Toyota', 'Honda', 'Mazda', 'Ford'],
['2017', 10, 11, 12, 13, 15, 16],
['2018', 10, 11, 12, 13, 15, 16],
['2019', 10, 11, 12, 13, 15, 16],
]}
rowHeaders={true}
colHeaders={true}
contextMenu={true}
comments={true}
cell={[
{ row: 1, col: 1, comment: { value: 'Some comment' } },
// add the `style` parameter
{
row: 2,
col: 2,
comment: {
value: 'Comment 200x50 px',
style: { width: 200, height: 50 },
},
},
]}
height="auto"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;

Set a delay for displaying comments

To display comments after a pre-configured time delay, use the displayDelay parameter.

JavaScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
return (
<HotTable
data={[
['', 'Tesla', 'Toyota', 'Honda', 'Ford'],
['2018', 10, 11, 12, 13, 15, 16],
['2019', 10, 11, 12, 13, 15, 16],
['2020', 10, 11, 12, 13, 15, 16],
]}
rowHeaders={true}
colHeaders={true}
contextMenu={true}
comments={{
// on mouseover, wait 2 seconds before the comment box displays
displayDelay: 2000,
}}
cell={[{ row: 1, col: 1, comment: { value: 'Some comment' } }]}
height="auto"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;
TypeScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
return (
<HotTable
data={[
['', 'Tesla', 'Toyota', 'Honda', 'Ford'],
['2018', 10, 11, 12, 13, 15, 16],
['2019', 10, 11, 12, 13, 15, 16],
['2020', 10, 11, 12, 13, 15, 16],
]}
rowHeaders={true}
colHeaders={true}
contextMenu={true}
comments={{
// on mouseover, wait 2 seconds before the comment box displays
displayDelay: 2000,
}}
cell={[{ row: 1, col: 1, comment: { value: 'Some comment' } }]}
height="auto"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;

Flag invalid cells with a comment

Combine comments with validation to explain data-entry errors. This example adds a comment to a cell when it fails validation and removes the comment once the value is valid. The afterValidate hook drives the change, and setCommentAtCell() writes the message. The grid validates on load, so the Mechanical keyboard row starts with an invalid stock value and shows its comment right away. Edit any Stock cell and enter a negative number or text to flag it, or enter a valid whole number to clear the flag.

JavaScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
return (<HotTable data={[
['Wireless mouse', 142],
['USB-C cable', 67],
['Mechanical keyboard', -5],
['Laptop stand', 38],
['HDMI adapter', 210],
]} colHeaders={['Product', 'Stock']} rowHeaders={true} comments={true} columns={[
{},
{
type: 'numeric',
validator(value, callback) {
callback(Number.isInteger(value) && value >= 0);
},
},
]}
// Attach a comment when a cell fails validation, and remove it once the cell is valid.
afterValidate={function (isValid, value, row, prop) {
const column = this.propToCol(prop);
const comments = this.getPlugin('comments');
if (!isValid) {
comments.setCommentAtCell(row, column, `"${value}" is not valid. Enter a whole number of 0 or more.`);
}
else {
comments.removeCommentAtCell(row, column);
}
}}
// Validate every cell on load so the pre-existing invalid value is flagged right away.
afterInit={function () {
this.validateCells();
}} height="auto" autoWrapRow={true} autoWrapCol={true} licenseKey="non-commercial-and-evaluation"/>);
};
export default ExampleComponent;
TypeScript
import Handsontable from 'handsontable/base';
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
return (
<HotTable
data={[
['Wireless mouse', 142],
['USB-C cable', 67],
['Mechanical keyboard', -5],
['Laptop stand', 38],
['HDMI adapter', 210],
]}
colHeaders={['Product', 'Stock']}
rowHeaders={true}
comments={true}
columns={[
{},
{
type: 'numeric',
validator(value: any, callback: (valid: boolean) => void) {
callback(Number.isInteger(value) && value >= 0);
},
},
]}
// Attach a comment when a cell fails validation, and remove it once the cell is valid.
afterValidate={function (this: Handsontable, isValid, value, row, prop) {
const column = this.propToCol(prop);
const comments = this.getPlugin('comments');
if (!isValid) {
comments.setCommentAtCell(row, column, `"${value}" is not valid. Enter a whole number of 0 or more.`);
} else {
comments.removeCommentAtCell(row, column);
}
}}
// Validate every cell on load so the pre-existing invalid value is flagged right away.
afterInit={function (this: Handsontable) {
this.validateCells();
}}
height="auto"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;

Read all comments programmatically

To collect every comment in the grid, loop through the rows and read each cell’s metadata with getCellMetaAtRow(). Each returned cell-meta object stores its comment under the comment key. Click List all comments to print all comments below the grid.

JavaScript
import { useRef, useState } from 'react';
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
const hotRef = useRef(null);
const [output, setOutput] = useState('');
const listComments = () => {
const hot = hotRef.current?.hotInstance;
if (!hot) {
return;
}
const found = [];
// `getCellMetaAtRow()` takes a physical row index (equal to the visual index here, with no sorting or trimming).
for (let row = 0; row < hot.countRows(); row += 1) {
hot.getCellMetaAtRow(row).forEach((cellMeta, col) => {
const comment = cellMeta.comment;
if (comment?.value !== undefined) {
found.push(`Row ${row + 1}, "${hot.getColHeader(col)}": ${comment.value}`);
}
});
}
setOutput(found.length > 0 ? found.join('\n') : 'No comments found.');
};
return (<>
<div className="example-controls-container">
<div className="controls">
<button id="list-comments" onClick={() => listComments()}>
List all comments
</button>
</div>
</div>
<HotTable ref={hotRef} data={[
['Update API docs', 'Ana García', 'In progress'],
['Deploy hotfix', 'James Okafor', 'Blocked'],
['Review pull requests', 'Li Wei', 'Done'],
['Plan Q3 roadmap', 'Maria Santos', 'In progress'],
['Refactor auth module', 'David Kim', 'In review'],
]} colHeaders={['Task', 'Assignee', 'Status']} rowHeaders={true} comments={true} cell={[
{ row: 1, col: 2, comment: { value: 'Waiting on infrastructure approval.' } },
{ row: 3, col: 1, comment: { value: 'Reassign if capacity is tight.' } },
{ row: 4, col: 0, comment: { value: 'Blocked on the security review.' } },
]} height="auto" autoWrapRow={true} autoWrapCol={true} licenseKey="non-commercial-and-evaluation"/>
<output className="comments-output" style={{ whiteSpace: 'pre-wrap' }}>
{output}
</output>
</>);
};
export default ExampleComponent;
TypeScript
import { useRef, useState } from 'react';
import { HotTable, HotTableRef } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
const hotRef = useRef<HotTableRef>(null);
const [output, setOutput] = useState('');
const listComments = () => {
const hot = hotRef.current?.hotInstance;
if (!hot) {
return;
}
const found: string[] = [];
// `getCellMetaAtRow()` takes a physical row index (equal to the visual index here, with no sorting or trimming).
for (let row = 0; row < hot.countRows(); row += 1) {
hot.getCellMetaAtRow(row).forEach((cellMeta, col) => {
const comment = cellMeta.comment as { value?: string } | undefined;
if (comment?.value !== undefined) {
found.push(`Row ${row + 1}, "${hot.getColHeader(col)}": ${comment.value}`);
}
});
}
setOutput(found.length > 0 ? found.join('\n') : 'No comments found.');
};
return (
<>
<div className="example-controls-container">
<div className="controls">
<button id="list-comments" onClick={() => listComments()}>
List all comments
</button>
</div>
</div>
<HotTable
ref={hotRef}
data={[
['Update API docs', 'Ana García', 'In progress'],
['Deploy hotfix', 'James Okafor', 'Blocked'],
['Review pull requests', 'Li Wei', 'Done'],
['Plan Q3 roadmap', 'Maria Santos', 'In progress'],
['Refactor auth module', 'David Kim', 'In review'],
]}
colHeaders={['Task', 'Assignee', 'Status']}
rowHeaders={true}
comments={true}
cell={[
{ row: 1, col: 2, comment: { value: 'Waiting on infrastructure approval.' } },
{ row: 3, col: 1, comment: { value: 'Reassign if capacity is tight.' } },
{ row: 4, col: 0, comment: { value: 'Blocked on the security review.' } },
]}
height="auto"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
<output className="comments-output" style={{ whiteSpace: 'pre-wrap' }}>
{output}
</output>
</>
);
};
export default ExampleComponent;

Result

Cells with comments display a small indicator in the corner. Users can view, edit, or delete comments through the context menu, and pre-configured comments appear when the table loads.

WindowsmacOSActionExcelSheets
Ctrl+Alt+M++MAdd or edit a comment
Ctrl+Enter+EnterSave and exit the current comment
EscapeEscapeExit the current comment without saving

Configuration options

Hooks

Plugins