useCellClipboard
Copies the active range to the OS clipboard as tab-separated text, so it
round-trips through Excel and Google Sheets. Paste reads TSV back, anchors it
at the top-left of the current range, validates every cell through the target
column's valueParser, and hands you one event per accepted cell. Cut marks
the range and clears the source cells only when the paste lands.
Live demo
Select a range, copy or cut it, select a destination, paste. Email is read-only and rejects pastes
Live
Quick example
import { useHeadlessGrid, useRangeSelection, useCellClipboard } from '@alaarab/ogrid-react';
const grid = useHeadlessGrid({ columns, data, getRowId });
const range = useRangeSelection({ rowCount: grid.rows.length, colCount: grid.columns.length });
const clipboard = useCellClipboard({
rangeSelection: range,
rows: grid.rows,
getRowId,
columns: grid.columns,
onCellEdit: (events) => setData((prev) => events.reduce(applyOneEvent, prev)),
onClipboardError: () => toast('Clipboard access was blocked'),
});
// On the focusable grid container. Ctrl/Cmd+C, X and V are left to the
// browser: the native copy, cut and paste events that follow carry the
// clipboard data, and onCopy / onCut / onPaste handle them.
<div
tabIndex={0}
onCopy={clipboard.onCopy}
onCut={clipboard.onCut}
onPaste={clipboard.onPaste}
onKeyDown={(e) => {
if (e.key === 'Escape') clipboard.clearClipboard();
}}
>
Parameters
UseCellClipboardParams<T>
| Param | Type | Description |
|---|---|---|
rangeSelection | UseRangeSelectionResult | Copy/cut source and paste anchor come from rangeSelection.range. |
rows | T[] | The rows you render (grid.rows). |
getRowId | (item: T) => RowId | Optional. Keeps a pending cut attached to its rows when the array is re-sorted or replaced. Defaults to object identity. |
columns | IColumnDef<T>[] | Visible columns, in render order. |
onCellEdit | (events: ICellValueChangedEvent<T>[]) => void | Called once per paste with the accepted cells, plus the cut-clear events if a cut was pending. |
clipboard | { readText(): Promise<string>; writeText(text): Promise<void> } | Optional override. Defaults to navigator.clipboard. Useful for tests or an in-memory fallback. |
onClipboardError | (error: unknown) => void | Called when the clipboard read or write throws (permission denied, insecure context). |
Returns
UseCellClipboardResult
| Field | Type | Description |
|---|---|---|
copyRange | () => Promise<void> | Programmatic copy for buttons and menus: write the range as TSV with clipboard.writeText, mark it as the copy range. No-op without a range. |
cutRange | () => Promise<void> | Programmatic cut: write as TSV with clipboard.writeText and mark the range as cut. Nothing is cleared yet. |
pasteRange | () => Promise<void> | Programmatic paste for buttons and menus: read TSV with clipboard.readText, apply at the range's top-left, fire onCellEdit, clear markers. |
onCopy | (event: CellClipboardCopyEvent) => void | Native copy handler for the container (onCopy={clipboard.onCopy}). Puts the range's TSV on event.clipboardData, calls preventDefault(), then marks the copy range like copyRange. Ignores copies aimed at an input, textarea or contenteditable inside the container, and the no-range case. |
onCut | (event: CellClipboardCopyEvent) => void | Native cut handler (onCut={clipboard.onCut}). Like onCopy, but marks the range as cut like cutRange. Nothing is cleared until the paste. |
onPaste | (event: CellClipboardPasteEvent) => void | Native paste handler for the container (onPaste={clipboard.onPaste}). Takes the text from event.clipboardData, calls preventDefault(), then pastes like pasteRange. Ignores pastes aimed at an input, textarea or contenteditable inside the container, events without text, and the no-range case. |
canPaste | boolean | navigator.clipboard.readText is available, so pasteRange() can read the clipboard. false on the server and during the first client render. onPaste works regardless. |
activeCopyRange | ISelectionRange | null | For marching-ants rendering. |
activeCutRange | ISelectionRange | null | For marching-ants rendering. |
clearClipboard | () => void | Drop both markers and the pending cut. Bind to Escape. |
Behavior notes
- The three programmatic actions never reject. Failures go to
onClipboardErrorand the promise resolves, soawait clipboard.pasteRange()is safe without atry/catch. - Copy text follows the column.
clipboardFormatterwins when set (its output is final, so returning''keeps a column off the clipboard); otherwisevalueFormatter; otherwise the raw value. Rows are joined with\r\n, cells with\t. - Paste anchors at the normalized top-left of the range, even when the
user selected upward or leftward. Cells that would land past the last row or
column are dropped; read-only columns and values rejected by
valueParserare skipped. - Cut clears only what was actually pasted. For each source cell, the corresponding destination must have accepted the value. Read-only destinations, rejected values, and out-of-bounds cells leave their source intact. When paste overlaps the cut source, the pasted value wins.
- A cut survives re-sorting but not an intervening copy. The cut source is
remembered by
getRowId(or object identity), so sorting between cut and paste still clears the right rows. If the clipboard text no longer matches what the cut wrote (the user copied something else), the paste applies normally and no cells are cleared. - Paste the keyboard shortcut through
onPaste, notpasteRange.navigator.clipboard.readTextis undefined on plain http, prompts in Chromium and is denied by default in Firefox and Safari, so a Ctrl+V handler that callspasteRange()silently fails there. The browser delivers the clipboard text with the nativepasteevent anyway;onPastereads it fromevent.clipboardDatawith no permission involved. Do not handle (orpreventDefault()) Ctrl/Cmd+V inonKeyDown: a prevented keydown suppresses the paste event, and callingpasteRange()as well pastes twice. Shift+Insert works the same way. - Copy and cut the keyboard shortcut through
onCopy/onCut. A Ctrl+C handler that callscopyRange()needsnavigator.clipboard.writeText, which is undefined on plain http, so the copy never reaches the OS clipboard there. Handling the nativecopy/cutevent puts the TSV onevent.clipboardDatainstead, with no permission or secure context needed. As with paste, do not handle orpreventDefault()Ctrl/Cmd+C or X inonKeyDown: a prevented keydown suppresses the event. copyRange,cutRange,pasteRangeandcanPasteare for buttons and menus. A toolbar or context-menu Copy, Cut or Paste has no native event to use, so it goes throughclipboard.writeText/readText; gate Paste oncanPaste. Pass aclipboardoverride if you want an in-app fallback for that path, as the demo above does.- Keyboard wiring is yours. Attach
onCopy,onCut,onPasteand any shortcuts to a focusable container (notdocument) so multiple grids on one page do not fight.
See also
useRangeSelectionprovides the rangeuseUndoRedowithbeginBatch/endBatchto undo a whole paste in one step- Editing & Clipboard: clipboard inside
<OGrid>