Skip to content
Spreadsheet Editor

Cell overlays

An overlay is a DOM node of yours that the editor places on a cell or a range. The editor keeps it there through scrolling, resizing and sheet switches. You decide what is inside it: an agent’s cursor, a collaborator’s selection, a suggested edit with an accept button.

Overlays are UI only. They are not part of the workbook and are never saved.

Overlays live on the controller. Create an element and hand it to overlays.add with a target. The id it returns is what update and remove take.

import { useRef } from "react";
import { SpreadsheetEditor, type SpreadsheetController } from "@grid-is/spreadsheet-editor";
import "@grid-is/spreadsheet-editor/style.css";
export default function App() {
const controllerRef = useRef<SpreadsheetController>(null);
const addCursor = () => {
const element = document.createElement("div");
element.style.border = "2px solid #7c3aed";
controllerRef.current?.overlays.add({ target: "B2", element });
};
return (
<>
<button onClick={addCursor}>Mark B2</button>
<SpreadsheetEditor model={model} controllerRef={controllerRef} />
</>
);
}

A target is a cell or a range, with or without a sheet name: "B2", "B2:D4", "Sheet2!B2".

The editor sets these styles on your element and nothing else:

  • position, left, top, width and height, so the element covers its target
  • pointer-events, from the interactive option

While its target cannot be shown, the element is taken out of the DOM. It is put back as soon as the target can be shown again.

Your content does not have to fit inside the target. The element covers its cells, and a name tag above a cursor or a card beside a cell can overflow it freely. Overflowing content is clipped at the edges of the grid, so it slides under the headers the same way cells do.

In React, render into the element with a portal:

import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
function Cursor({ controllerRef, name }) {
const [element] = useState(() => document.createElement("div"));
useEffect(() => {
const overlays = controllerRef.current?.overlays;
if (!overlays) return;
const id = overlays.add({ target: "B2", element });
return () => overlays.remove(id);
}, [controllerRef, element]);
return createPortal(<span className="name-tag">{name}</span>, element);
}

Render the component after the editor has mounted, so the controller is there when the effect runs.

By default an overlay is a highlight. Clicks, double-clicks and drags go through it to the cells underneath.

Set interactive: true when the overlay has buttons or inputs. Clicks then stop at the overlay and the sheet does not react to them. The mouse wheel scrolls the grid either way.

controllerRef.current?.overlays.add({ target: "C12", element, interactive: true });

Clicking a button in an overlay moves keyboard focus to that button. Call focus() to hand it back to the sheet.

The suggested edit in the demo above is an interactive overlay on the cell. Accept writes the value through the model, removes the overlay and gives focus back to the sheet:

const accept = () => {
const ref = new Reference("C12");
model.getWorkbook()?.writeCellData(ref, { v: 1800 });
model.recalculate();
controllerRef.current?.overlays.remove(id);
controllerRef.current?.focus(ref.toString());
};
update(id: string, patch: Partial<SpreadsheetOverlay>): void

Changes an overlay. Use it to move an overlay to another target. An id that does not exist is ignored.

controllerRef.current?.overlays.update(id, { target: "D6" });
remove(id: string): void

Takes the overlay off the sheet. Your element is left as it is.

controllerRef.current?.overlays.remove(id);
all(): Array<SpreadsheetOverlay & { id: string }>

Returns every overlay with its id, in the order they were added.

const targets = controllerRef.current?.overlays.all().map((overlay) => overlay.target);
clear(): void

Removes every overlay.

controllerRef.current?.overlays.clear();
  • Sheets. A target with a sheet name, like "Sheet2!B2", is out of the DOM while another sheet is open. A target without one, like "B2", shows on whichever sheet is open.
  • Merged cells. A target is taken as written. "B2" covers only B2’s own slot, also when B2 is part of a merged cell. Pass the whole range, like "B2:D4", to cover the merged cell.
  • Hidden rows and columns. An overlay is out of the DOM while its target has no size.
  • A new model. Overlays stay when the model prop changes, so an overlay you keep updating carries on working. Call clear, or remove the ones you own, when you load another workbook.
  • Inserted rows and columns. A target stays at its address. Insert a row above an overlay on B2 and it is still on B2. Listen for events and call update if it should follow the content.