> ## 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.

# DragHandle

> Add drag handles to blocks for intuitive drag-and-drop reordering

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

<CodeGroup>
  ```bash npm theme={null}
  npm install @tiptap/extension-drag-handle
  ```

  ```bash yarn theme={null}
  yarn add @tiptap/extension-drag-handle
  ```

  ```bash pnpm theme={null}
  pnpm add @tiptap/extension-drag-handle
  ```
</CodeGroup>

## Basic Usage

```javascript theme={null}
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

```css theme={null}
.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

<ParamField path="render" type="() => HTMLElement" required>
  Function that renders and returns the drag handle element.

  ```javascript theme={null}
  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
    },
  })
  ```
</ParamField>

<ParamField path="computePositionConfig" type="ComputePositionConfig">
  Configuration for position computation using the Floating UI library. See [Floating UI documentation](https://floating-ui.com/docs/computePosition) for details.

  **Default:**

  ```javascript theme={null}
  {
    placement: 'left-start',
    strategy: 'absolute',
  }
  ```

  **Example:**

  ```javascript theme={null}
  DragHandle.configure({
    render() { /* ... */ },
    computePositionConfig: {
      placement: 'right-start',
      strategy: 'fixed',
    },
  })
  ```
</ParamField>

<ParamField path="getReferencedVirtualElement" type="() => 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.

  ```javascript theme={null}
  DragHandle.configure({
    render() { /* ... */ },
    getReferencedVirtualElement: () => ({
      getBoundingClientRect: () => customElement.getBoundingClientRect(),
    }),
  })
  ```
</ParamField>

<ParamField path="locked" type="boolean">
  Whether the drag handle is locked in place and visibility.

  **Default:** `false`

  ```javascript theme={null}
  DragHandle.configure({
    render() { /* ... */ },
    locked: true, // Handle stays visible and in position
  })
  ```
</ParamField>

<ParamField path="onNodeChange" type="function">
  Callback function called when a node is hovered over or unhovered.

  **Parameters:**

  * `node`: The hovered node (or null if unhovered)
  * `editor`: The editor instance

  ```javascript theme={null}
  DragHandle.configure({
    render() { /* ... */ },
    onNodeChange: ({ node, editor }) => {
      if (node) {
        console.log('Hovering over:', node.type.name)
      } else {
        console.log('No longer hovering')
      }
    },
  })
  ```
</ParamField>

<ParamField path="onElementDragStart" type="(e: DragEvent) => void">
  Callback fired when dragging starts.

  ```javascript theme={null}
  DragHandle.configure({
    render() { /* ... */ },
    onElementDragStart: (e) => {
      console.log('Drag started')
      document.body.classList.add('is-dragging')
    },
  })
  ```
</ParamField>

<ParamField path="onElementDragEnd" type="(e: DragEvent) => void">
  Callback fired when dragging ends.

  ```javascript theme={null}
  DragHandle.configure({
    render() { /* ... */ },
    onElementDragEnd: (e) => {
      console.log('Drag ended')
      document.body.classList.remove('is-dragging')
    },
  })
  ```
</ParamField>

<ParamField path="nested" type="boolean | NestedOptions">
  Enable drag handles for nested content like list items and blockquotes.

  **Default:** `false`

  **Simple enable:**

  ```javascript theme={null}
  DragHandle.configure({
    render() { /* ... */ },
    nested: true,
  })
  ```

  **With configuration:**

  ```javascript theme={null}
  DragHandle.configure({
    render() { /* ... */ },
    nested: {
      allowedContainers: ['bulletList', 'orderedList'],
      edgeDetection: 'left',
      defaultRules: true,
    },
  })
  ```

  **Nested Options:**

  <ParamField path="nested.rules" type="DragHandleRule[]">
    Custom rules to determine which nodes are draggable. These run after the default rules.

    ```javascript theme={null}
    nested: {
      rules: [{
        id: 'excludeCodeBlocks',
        evaluate: ({ node }) => {
          return node.type.name === 'codeBlock' ? 1000 : 0
        },
      }],
    }
    ```
  </ParamField>

  <ParamField path="nested.defaultRules" type="boolean">
    Whether to include default rules for common cases like list items.

    **Default:** `true`
  </ParamField>

  <ParamField path="nested.allowedContainers" type="string[]">
    Restrict nested dragging to specific container types.

    ```javascript theme={null}
    nested: {
      allowedContainers: ['bulletList', 'orderedList', 'blockquote'],
    }
    ```
  </ParamField>

  <ParamField path="nested.edgeDetection" type="'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:**

    ```javascript theme={null}
    nested: {
      edgeDetection: {
        edges: ['left', 'top'],
        threshold: 12,  // pixels from edge
        strength: 500,  // scoring strength
      },
    }
    ```
  </ParamField>
</ParamField>

## Commands

<ParamField path="lockDragHandle" type="command">
  Lock the drag handle in place and visibility.

  ```javascript theme={null}
  editor.commands.lockDragHandle()
  ```
</ParamField>

<ParamField path="unlockDragHandle" type="command">
  Unlock the drag handle.

  ```javascript theme={null}
  editor.commands.unlockDragHandle()
  ```
</ParamField>

<ParamField path="toggleDragHandle" type="command">
  Toggle the drag handle lock state.

  ```javascript theme={null}
  editor.commands.toggleDragHandle()
  ```
</ParamField>

## Advanced Examples

### Custom Drag Handle with Icon

```javascript theme={null}
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

```javascript theme={null}
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

```javascript theme={null}
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

```javascript theme={null}
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)

```javascript theme={null}
DragHandle.configure({
  render() { /* ... */ },
  computePositionConfig: {
    placement: 'right-start',
    strategy: 'absolute',
  },
  nested: {
    edgeDetection: 'right', // Prefer parent near right edge
  },
})
```

### Lock/Unlock Programmatically

```javascript theme={null}
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

```tsx theme={null}
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

```css theme={null}
.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

```css theme={null}
/* 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:

* [Extension](https://github.com/ueberdosis/tiptap/tree/main/packages/extension-drag-handle/src/drag-handle.ts:117)
* [Plugin](https://github.com/ueberdosis/tiptap/tree/main/packages/extension-drag-handle/src/drag-handle-plugin.ts)
* [Types](https://github.com/ueberdosis/tiptap/tree/main/packages/extension-drag-handle/src/types/options.ts:40)
