Skip to content

Row Drag-Drop Plugin

The Row Drag-Drop plugin lets users rearrange rows by dragging a grip handle or using keyboard shortcuts, and — when opted in via dropZone — drag rows between separate grids. It is a strict superset of the (now deprecated) RowReorderPlugin: every existing intra-grid behaviour is preserved.

  • Intra-grid — drag a row up or down to reorder, or use Ctrl + ↑/↓.
  • Drag origin — choose whether drags start from the dedicated handle column (dragFrom: 'handle', default), from anywhere on the row (dragFrom: 'row' — handle column hidden), or from both (dragFrom: 'both'). Interactive descendants (buttons, inputs, links, [contenteditable]) inside the row are still respected and never start a drag.
  • Cross-grid — set a dropZone to allow dragging rows to another grid with the same dropZone. Supports move and copy operations.
  • Multi-row — when the Selection plugin is loaded and the dragged row is part of a multi-row selection, all selected rows are dragged together and a count badge appears on the drag image. There is no selection config option — the behaviour is automatic.
  • Cross-window — drag rows between separate browser windows or tabs on the same origin. The payload travels via dataTransfer, and a same-origin BroadcastChannel coordinates the source-side row removal for operation: 'move' and the source-side row-transfer event.
import '@toolbox-web/grid/features/row-drag-drop';

The legacy reorderRows feature key continues to work and forwards to the same plugin instance.

Enable the feature and a drag handle column appears automatically. Users can drag rows to new positions or use Ctrl + ↑/↓ to move the focused row.

import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/row-drag-drop';
const grid = queryGrid('tbw-grid');
grid.gridConfig = {
columns: [
{ field: 'priority', header: '#', type: 'number', width: 50 },
{ field: 'task', header: 'Task' },
{ field: 'status', header: 'Status' },
],
features: {
rowDragDrop: {
dragHandlePosition: 'left',
enableKeyboard: true,
animation: 'flip',
},
},
};
// Listen for row moves (intra-grid)
grid.on('row-move', ({ fromIndex, toIndex }) => {
console.log(`Moved row from ${fromIndex} to ${toIndex}`);
});
Drag from Where the drag can be initiated from
Handle position Position of drag handle column
Keyboard enabled Allow reorder with keyboard shortcuts
Animation Reorder animation style

Drag the grip handle (☰) to reorder rows. Use Ctrl + ↑/↓ to move the focused row with the keyboard.

Try moving "Bob" - it will be blocked!

Cancel row moves by calling event.preventDefault() in the row-move handler.

The grids below share dropZone: 'employees' with operation: 'move'. Drag any row from the left grid to the right grid (or back).

Available employees

Selected employees

Grids in different browser windows (or tabs) on the same origin can also exchange rows. The button in the demo below opens a popout window that hosts a target grid joined to the same dropZone. Drag any row from the left grid into the popout — the row is moved across windows, and the source grid’s row-transfer event fires once the popout confirms the drop.

Available employees

Click the button to open the target grid in a new browser window. Then drag rows from this grid into the popout. Both grids must run on the same origin; popup blockers may interfere.

The popout page itself is a tiny standalone host that registers the same dropZone (pages/grid/row-drag-drop-popout.astro).

Under the hood the plugin sets a custom MIME type on dataTransfer and opens a BroadcastChannel('tbw-row-drag-drop'). The target window decodes the payload from dataTransfer and broadcasts a confirmation; the source window’s plugin instance listens on the same channel and — on move — removes the transferred rows and emits row-transfer locally.

Custom row shapes. Rows are round-tripped through JSON.parse(JSON.stringify(row)) by default. If your rows contain non-JSON values (functions, Date, Map, etc.), provide serializeRow / deserializeRow so the target reconstructs an equivalent object:

features: {
rowDragDrop: {
dropZone: 'employees',
serializeRow: (row) => ({ ...row, hiredAt: row.hiredAt.toISOString() }),
deserializeRow: (raw) => ({ ...raw, hiredAt: new Date(raw.hiredAt) }),
},
}

Caveats.

  • Cross-window coordination requires BroadcastChannel; in environments without it (very old browsers, sandboxed iframes), only the target receives rows and the source is left untouched.
  • Both windows must be on the same origin — BroadcastChannel is origin-scoped.
  • The source’s row-drag-end may fire with accepted: false before the remote confirmation arrives. Treat the row-transfer event as the authoritative success signal for cross-window transfers.

See RowDragDropConfig for the full list of options and defaults. Key options:

OptionTypeDescription
dropZonestringOpt-in identifier for cross-grid drops. Grids with the same value accept each other.
operation'move' | 'copy'Whether the source grid keeps the rows after a successful cross-grid drop.
canDrop(payload, targetIndex) => booleanReject specific drops on the target grid. Called synchronously during dragover and at drop time.
canDrag(row, index) => booleanReject drags from the source. Called once at dragstart with the originating row.
serializeRow(row) => unknownOverride the JSON shape used in the cross-window fallback payload.
deserializeRow(serialized) => rowInverse of serializeRow.
enableKeyboardbooleanKeep keyboard reorder (Ctrl + ↑/↓) — defaults to true.
dragFrom'handle' | 'row' | 'both'Where a drag can be initiated. 'handle' (default) keeps the handle column; 'row' hides the handle and lets users drag anywhere on the row; 'both' allows either. Interactive descendants (button, input, select, textarea, a[href], [contenteditable]) never start a drag.
showDragHandlebooleanRender the drag-handle column. Defaults to true for dragFrom: 'handle' | 'both' and false for dragFrom: 'row'. Set explicitly to override.
dragHandlePosition'left' | 'right'Pin the handle column to either side.
dragHandleWidthnumberPixel width of the handle column.
animation'flip' | falseFLIP animation on intra-grid reorder.
autoScrollboolean | { edgeSize?: number; speed?: number; maxSpeed?: number }Tune the auto-scroll behaviour during drag.
EventDetailCancelableWhen
row-moveRowMoveDetailYesIntra-grid reorder committed (back-compat).
row-drag-startRowDragStartDetailYesA drag begins.
row-drag-endRowDragEndDetailNoThe drag ends, regardless of whether a drop succeeded.
row-dropRowDropDetailYesRows are dropped on this grid from another grid.
row-transferRowTransferDetailNoSuccessful cross-grid transfer (fired on both grids).
KeyAction
Ctrl + ↑Move focused row up
Ctrl + ↓Move focused row down

The plugin adds these CSS classes you can customise:

ClassDescription
.dg-row-drag-handleThe drag handle element
.dg-row-drag-handle:hoverHandle hover state
.data-grid-row.draggingRow currently being dragged
.data-grid-row.drop-targetRow being hovered as drop target
.data-grid-row.drop-beforeDrop indicator showing insertion above
.data-grid-row.drop-afterDrop indicator showing insertion below
.tbw-grid--drag-sourceThe source grid during a drag (whole-grid state)
.tbw-grid--drop-target-activeThe target grid during a valid dragover
.tbw-grid--drop-target-rejectedThe target grid during a canDrop-rejected drag
.tbw-grid--auto-scrollingA grid currently auto-scrolling during a drag

Override the default grip icon via CSS:

.dg-row-drag-handle::before {
content: '⋮⋮'; /* Double vertical ellipsis */
}
  • Row identification — currently uses processed/visual row indices (data-row attribute and payload.rowIndices), not getRowId(). Same-window cross-grid drops also recover live object references via the internal WeakRef registry; primitive row values fall through to the JSON payload.
  • Data mutation — the plugin reorders grid.rows in place. The row-move event provides the updated array.
  • Virtualization — works with virtualized grids; only visible rows are rendered but logical indices are preserved.
  • Keyboard debounce — rapid keyboard moves are debounced (150 ms) to prevent excessive rerenders.
  • Drag image — when a drag starts the plugin clones the source row and uses it as the HTML5 drag image so the user sees the full row content follow the cursor (instead of just the handle cell). The browser applies its standard translucency to the snapshot — this is a native effect that cannot be disabled.
  • Cross-window fallback — when the live row reference cannot be recovered from the same-window registry (e.g. drop into a different window), the row payload is decoded from dataTransfer JSON via serializeRow/deserializeRow. The source window completes its half of the transfer (row removal on move, row-transfer emit) when it receives the confirmation message on the tbw-row-drag-drop BroadcastChannel.

RowReorderPlugin is now an alias for RowDragDropPlugin. Existing code keeps working until V3 — both reorderRows and rowDragDrop feature keys resolve to the same plugin instance, and merging conflicting configs across the two keys will throw at attach time. To migrate:

import { RowReorderPlugin } from '@toolbox-web/grid/plugins/reorder-rows';
import { RowDragDropPlugin } from '@toolbox-web/grid/plugins/row-drag-drop';
new RowReorderPlugin(cfg);
new RowDragDropPlugin(cfg);

The legacy canMove callback continues to work — it is mapped internally to canDrop with a synthesised intra-grid payload.

AI assistants: For complete API documentation, implementation guides, and code examples for this library, see https://raw.githubusercontent.com/OysteinAmundsen/toolbox/main/llms-full.txt