Skip to main content

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>

ParamTypeDescription
rangeSelectionUseRangeSelectionResultCopy/cut source and paste anchor come from rangeSelection.range.
rowsT[]The rows you render (grid.rows).
getRowId(item: T) => RowIdOptional. Keeps a pending cut attached to its rows when the array is re-sorted or replaced. Defaults to object identity.
columnsIColumnDef<T>[]Visible columns, in render order.
onCellEdit(events: ICellValueChangedEvent<T>[]) => voidCalled 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) => voidCalled when the clipboard read or write throws (permission denied, insecure context).

Returns​

UseCellClipboardResult

FieldTypeDescription
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) => voidNative 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) => voidNative cut handler (onCut={clipboard.onCut}). Like onCopy, but marks the range as cut like cutRange. Nothing is cleared until the paste.
onPaste(event: CellClipboardPasteEvent) => voidNative 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.
canPastebooleannavigator.clipboard.readText is available, so pasteRange() can read the clipboard. false on the server and during the first client render. onPaste works regardless.
activeCopyRangeISelectionRange | nullFor marching-ants rendering.
activeCutRangeISelectionRange | nullFor marching-ants rendering.
clearClipboard() => voidDrop both markers and the pending cut. Bind to Escape.

Behavior notes​

  • The three programmatic actions never reject. Failures go to onClipboardError and the promise resolves, so await clipboard.pasteRange() is safe without a try/catch.
  • Copy text follows the column. clipboardFormatter wins when set (its output is final, so returning '' keeps a column off the clipboard); otherwise valueFormatter; 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 valueParser are 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, not pasteRange. navigator.clipboard.readText is undefined on plain http, prompts in Chromium and is denied by default in Firefox and Safari, so a Ctrl+V handler that calls pasteRange() silently fails there. The browser delivers the clipboard text with the native paste event anyway; onPaste reads it from event.clipboardData with no permission involved. Do not handle (or preventDefault()) Ctrl/Cmd+V in onKeyDown: a prevented keydown suppresses the paste event, and calling pasteRange() as well pastes twice. Shift+Insert works the same way.
  • Copy and cut the keyboard shortcut through onCopy / onCut. A Ctrl+C handler that calls copyRange() needs navigator.clipboard.writeText, which is undefined on plain http, so the copy never reaches the OS clipboard there. Handling the native copy / cut event puts the TSV on event.clipboardData instead, with no permission or secure context needed. As with paste, do not handle or preventDefault() Ctrl/Cmd+C or X in onKeyDown: a prevented keydown suppresses the event.
  • copyRange, cutRange, pasteRange and canPaste are for buttons and menus. A toolbar or context-menu Copy, Cut or Paste has no native event to use, so it goes through clipboard.writeText / readText; gate Paste on canPaste. Pass a clipboard override if you want an in-app fallback for that path, as the demo above does.
  • Keyboard wiring is yours. Attach onCopy, onCut, onPaste and any shortcuts to a focusable container (not document) so multiple grids on one page do not fight.

See also​