tiptap-extension-global-drag-handle

Drag-and-drop block reordering for Tiptap editors, Notion-style

Library
npm
v0.1.18
153stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
33/100Needs Attention
Development Activity0
Maintenance0
Community56
Maturity48
Momentum28

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
65/100Good
Architecture68
Code Quality45
Innovation78
Learning Curve55

tiptap-extension-global-drag-handle is a Tiptap extension that adds a floating, Notion-style drag handle to every block-level node in a Tiptap-powered rich text editor. Instead of wiring up custom drag-and-drop logic on top of ProseMirror’s selection and transaction APIs, developers register the extension and immediately get hover-to-reveal handles that let end users grab a paragraph, list item, heading, or blockquote and drop it elsewhere in the document.

The extension is headless by default (no bundled CSS), tracks mouse position to resolve the nearest draggable node via elementsFromPoint, and serializes the dragged selection to the clipboard so native HTML5 drag events behave correctly across browsers. It exposes a small set of configuration options — drag handle width, auto-scroll threshold, a custom handle selector, excluded tags, and custom node types — so it can be adapted to bespoke editor UIs and custom Tiptap node schemas without forking the extension.

What You Get

  • A GlobalDragHandle Tiptap Extension that can be dropped straight into an editor’s extensions array
  • Automatic detection of the block under the cursor (paragraphs, list items, headings, blockquotes, pre, and custom nodes) via nodeDOMAtCoords
  • A headless drag handle element with CSS-hook classes (.drag-handle, .hide, .dragging) so styling is fully left to the consuming app
  • Configurable auto-scroll near the viewport edges while dragging long documents
  • Support for a custom drag handle selector (e.g. a .custom-drag-handle element you already render) instead of the auto-generated one
  • List-aware drop handling that preserves ordered/unordered list wrapping when a list item is dropped outside its original list

Common Use Cases

  • Notion-style block editors where end users need to visually reorder paragraphs, headings, and lists by dragging
  • Internal CMS or documentation authoring tools built on Tiptap that need block reordering without hand-rolling ProseMirror drag logic
  • Collaborative document editors that want a consistent drag affordance across custom node types (alerts, callouts, embeds) via the customNodes option
  • Editors that already render their own drag-handle UI element and just need the drag/reorder mechanics wired up via dragHandleSelector

Under The Hood

Architecture — The package ships two files: src/index.ts, which defines the DragHandlePlugin ProseMirror plugin factory and the GlobalDragHandle Tiptap Extension.create() wrapper, and src/clipboard-serializer.ts, a small compatibility shim. The plugin’s view() lifecycle creates (or adopts, via dragHandleSelector) a floating handle element, attaches dragstart/drag listeners to it, and registers ProseMirror handleDOMEvents for mousemove, drop, dragstart, and dragend on the editor view itself. On mousemove, nodeDOMAtCoords walks document.elementsFromPoint against a fixed selector list (li, p:not(:first-child), pre, blockquote, h1-h6, plus any customNodes) to find the block under the cursor, then positions the handle using getBoundingClientRect math (absoluteRect) that also accounts for elements inside a [role="dialog"] with a CSS transform. On dragstart, it resolves the dragged node’s ProseMirror position (nodePosAtDOMcalcNodePos), converts the current selection into a NodeSelection, and manually populates event.dataTransfer using serializeForClipboard so native browser drag-and-drop carries a valid ProseMirror slice.

Tech Stack — Written in strict-mode TypeScript (ES2022 target, noUncheckedIndexedAccess enabled) against @tiptap/core (peer dep >=2.1.0) and @tiptap/pm (ProseMirror’s state/model/view modules re-exported by Tiptap). Built with Rollup (rollup-plugin-typescript2, @rollup/plugin-babel, rollup-plugin-auto-external) into three output formats — CJS, ESM, and UMD — with source maps, and @tiptap/core/@tiptap/pm/* marked external so the extension doesn’t bundle its own copy of the editor. No runtime dependencies beyond the Tiptap peer deps.

Code Quality — There is no test suite anywhere in the repository (no test/spec files, no CI configuration) — correctness relies entirely on manual verification and community bug reports/PRs. The code itself is compact and mostly readable, using consistent camelCase naming and small single-purpose helper functions (absoluteRect, nodeDOMAtCoords, nodePosAtDOM, calcNodePos), but the core mousemove and handleDragStart handlers are dense, minimally commented, and mix DOM measurement, ProseMirror position resolution, and selection-state mutation in the same function bodies — the kind of logic that benefits from tests it doesn’t have.

API Design — Integration is a single line: add GlobalDragHandle to the Tiptap extensions array, or call .configure({...}) for non-default behavior. All configuration options ship with defaults (dragHandleWidth: 20, scrollTreshold: 100, empty excludedTags/customNodes), so zero-config usage works out of the box, and the options object is fully typed via the exported GlobalDragHandleOptions interface. The main documentation gap is the lack of TSDoc/JSDoc on the plugin internals and no dedicated examples directory — usage guidance lives entirely in the README.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search