Documentation Index
Fetch the complete documentation index at: https://mintlify.com/ueberdosis/tiptap/llms.txt
Use this file to discover all available pages before exploring further.
The DragHandle extension adds a draggable handle to blocks in your editor, allowing users to reorder content by dragging. It uses Floating UI for intelligent positioning and supports nested content like list items and blockquotes.
Installation
npm install @tiptap/extension-drag-handle
Basic Usage
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import { DragHandle } from '@tiptap/extension-drag-handle'
const editor = new Editor({
extensions: [
StarterKit,
DragHandle.configure({
render() {
const element = document.createElement('div')
element.classList.add('drag-handle')
element.innerHTML = '⋮⋮'
return element
},
}),
],
})
Styling the Drag Handle
.drag-handle {
position: absolute;
cursor: grab;
padding: 4px;
color: #999;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
user-select: none;
}
.drag-handle:hover {
color: #333;
background: #f5f5f5;
}
.drag-handle:active {
cursor: grabbing;
}
Configuration Options
render
() => HTMLElement
required
Function that renders and returns the drag handle element.DragHandle.configure({
render() {
const element = document.createElement('div')
element.classList.add('drag-handle')
element.innerHTML = `
<svg width="10" height="10">
<circle cx="5" cy="5" r="1" fill="currentColor"/>
<circle cx="5" cy="2" r="1" fill="currentColor"/>
<circle cx="5" cy="8" r="1" fill="currentColor"/>
</svg>
`
return element
},
})
Configuration for position computation using the Floating UI library. See Floating UI documentation for details.Default:{
placement: 'left-start',
strategy: 'absolute',
}
Example:DragHandle.configure({
render() { /* ... */ },
computePositionConfig: {
placement: 'right-start',
strategy: 'fixed',
},
})
getReferencedVirtualElement
() => VirtualElement | null
Function that returns a virtual element for positioning. Useful when the drag handle needs to be positioned relative to a specific DOM element.DragHandle.configure({
render() { /* ... */ },
getReferencedVirtualElement: () => ({
getBoundingClientRect: () => customElement.getBoundingClientRect(),
}),
})
Whether the drag handle is locked in place and visibility.Default: falseDragHandle.configure({
render() { /* ... */ },
locked: true, // Handle stays visible and in position
})
Callback function called when a node is hovered over or unhovered.Parameters:
node: The hovered node (or null if unhovered)
editor: The editor instance
DragHandle.configure({
render() { /* ... */ },
onNodeChange: ({ node, editor }) => {
if (node) {
console.log('Hovering over:', node.type.name)
} else {
console.log('No longer hovering')
}
},
})
Callback fired when dragging starts.DragHandle.configure({
render() { /* ... */ },
onElementDragStart: (e) => {
console.log('Drag started')
document.body.classList.add('is-dragging')
},
})
Callback fired when dragging ends.DragHandle.configure({
render() { /* ... */ },
onElementDragEnd: (e) => {
console.log('Drag ended')
document.body.classList.remove('is-dragging')
},
})
Enable drag handles for nested content like list items and blockquotes.Default: falseSimple enable:DragHandle.configure({
render() { /* ... */ },
nested: true,
})
With configuration:DragHandle.configure({
render() { /* ... */ },
nested: {
allowedContainers: ['bulletList', 'orderedList'],
edgeDetection: 'left',
defaultRules: true,
},
})
Nested Options:Custom rules to determine which nodes are draggable. These run after the default rules.nested: {
rules: [{
id: 'excludeCodeBlocks',
evaluate: ({ node }) => {
return node.type.name === 'codeBlock' ? 1000 : 0
},
}],
}
Whether to include default rules for common cases like list items.Default: true
Restrict nested dragging to specific container types.nested: {
allowedContainers: ['bulletList', 'orderedList', 'blockquote'],
}
nested.edgeDetection
'left' | 'right' | 'both' | 'none'
Control when to prefer parent over nested node based on cursor position.
'left' (default): Prefer parent near left/top edges
'right': Prefer parent near right/top edges (for RTL)
'both': Prefer parent near any horizontal edge
'none': Disable edge detection
Or pass a config object:nested: {
edgeDetection: {
edges: ['left', 'top'],
threshold: 12, // pixels from edge
strength: 500, // scoring strength
},
}
Commands
Lock the drag handle in place and visibility.editor.commands.lockDragHandle()
Unlock the drag handle.editor.commands.unlockDragHandle()
Toggle the drag handle lock state.editor.commands.toggleDragHandle()
Advanced Examples
Custom Drag Handle with Icon
DragHandle.configure({
render() {
const element = document.createElement('div')
element.classList.add('custom-drag-handle')
element.innerHTML = `
<svg width="20" height="20" viewBox="0 0 20 20">
<path d="M7 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 2zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 14zm6-8a2 2 0 1 0-.001-4.001A2 2 0 0 0 13 6zm0 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 14z" fill="currentColor"/>
</svg>
`
return element
},
})
Track Drag Events
DragHandle.configure({
render() {
const element = document.createElement('div')
element.classList.add('drag-handle')
element.innerHTML = '⋮⋮'
return element
},
onElementDragStart: (e) => {
console.log('Started dragging')
// Add visual feedback
document.body.classList.add('is-dragging')
},
onElementDragEnd: (e) => {
console.log('Finished dragging')
// Remove visual feedback
document.body.classList.remove('is-dragging')
// Track analytics
analytics.track('block_reordered')
},
onNodeChange: ({ node }) => {
if (node) {
console.log('Hovering node:', node.type.name)
}
},
})
Enable Nested Dragging for Lists
DragHandle.configure({
render() {
const element = document.createElement('div')
element.classList.add('drag-handle')
element.innerHTML = '⋮⋮'
return element
},
nested: {
allowedContainers: ['bulletList', 'orderedList'],
edgeDetection: 'left',
},
})
Custom Rules for Nested Dragging
DragHandle.configure({
render() { /* ... */ },
nested: {
rules: [
{
id: 'preferListItems',
evaluate: ({ node }) => {
// Prefer list items over lists
if (node.type.name === 'listItem') return -100
if (node.type.name === 'bulletList') return 100
return 0
},
},
{
id: 'excludeCodeBlocks',
evaluate: ({ node }) => {
// Make code blocks undraggable
if (node.type.name === 'codeBlock') return 1000
return 0
},
},
],
defaultRules: true, // Keep default rules
},
})
Right-Side Placement (RTL)
DragHandle.configure({
render() { /* ... */ },
computePositionConfig: {
placement: 'right-start',
strategy: 'absolute',
},
nested: {
edgeDetection: 'right', // Prefer parent near right edge
},
})
Lock/Unlock Programmatically
const editor = new Editor({
extensions: [
StarterKit,
DragHandle.configure({
render() { /* ... */ },
}),
],
})
// Lock handle during certain operations
function performSensitiveOperation() {
editor.commands.lockDragHandle()
// Do operation...
editor.commands.unlockDragHandle()
}
// Toggle lock with button
document.querySelector('#toggle-drag').addEventListener('click', () => {
editor.commands.toggleDragHandle()
})
React Integration
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { DragHandle } from '@tiptap/extension-drag-handle'
import { useRef } from 'react'
function Editor() {
const dragHandleRef = useRef<HTMLDivElement>(null)
const editor = useEditor({
extensions: [
StarterKit,
DragHandle.configure({
render() {
const element = document.createElement('div')
element.className = 'drag-handle'
element.innerHTML = '⋮⋮'
return element
},
onNodeChange: ({ node }) => {
if (node) {
console.log('Hovering:', node.type.name)
}
},
}),
],
})
return (
<div>
<button onClick={() => editor?.commands.toggleDragHandle()}>
Toggle Drag Handle Lock
</button>
<EditorContent editor={editor} />
</div>
)
}
Styling Examples
Animated Drag Handle
.drag-handle {
position: absolute;
cursor: grab;
padding: 4px 6px;
color: #999;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
transition: all 0.15s ease;
opacity: 0;
}
/* Show on hover */
*:hover > .drag-handle {
opacity: 1;
}
.drag-handle:hover {
color: #333;
background: #f5f5f5;
transform: scale(1.1);
}
.drag-handle:active {
cursor: grabbing;
transform: scale(1);
}
/* When dragging */
body.is-dragging .drag-handle {
opacity: 0.5;
}
Nested Content Styling
/* Different styles for nested items */
.ProseMirror li > .drag-handle {
left: -30px;
}
.ProseMirror blockquote > .drag-handle {
left: -35px;
color: #666;
}
Source Code
View the source code on GitHub: