Skip to content
Spreadsheet Viewer

Cell Overlays

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

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

The API is the same as the editor’s, so overlay code written for one works with the other.

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 { SpreadsheetViewer } from "@grid-is/spreadsheet-viewer";
import type { SpreadsheetViewerController } from "@grid-is/spreadsheet-viewer";
function App() {
const controllerRef = useRef<SpreadsheetViewerController>(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>
<SpreadsheetViewer model={model} controllerRef={controllerRef} />
</>
);
}

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

The viewer 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 viewer 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: "I5", element, interactive: true });

The note in the demo above is an interactive overlay on the cell. Resolve removes the overlay:

const resolve = () => {
controllerRef.current?.overlays.remove(id);
};
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 "Summary!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. The viewer draws hidden rows and columns at their default size, so an overlay on one stays in place.
  • 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.

The types come from @grid-is/editor-types, shared with the editor: