Skip to main content
The Block Handle component provides interactive controls that appear beside blocks in your editor. It enables users to drag blocks to reorder content and add new blocks with a single click.

Features

  • Drag-and-drop block reordering
  • Quick block insertion
  • Visual drop indicators
  • Appears on hover
  • Customizable positioning
  • Framework-agnostic

Installation

import {
  BlockHandlePopover,
  BlockHandleAdd,
  BlockHandleDraggable,
} from 'prosekit/react/block-handle'
import { DropIndicator } from 'prosekit/react/drop-indicator'

Basic Usage

import {
  BlockHandlePopover,
  BlockHandleAdd,
  BlockHandleDraggable,
} from 'prosekit/react/block-handle'
import { DropIndicator } from 'prosekit/react/drop-indicator'

export default function Editor() {
  return (
    <ProseKit editor={editor}>
      <div ref={editor.mount} />
      
      {/* Block handle */}
      <BlockHandlePopover>
        <BlockHandleAdd>
          <div className="icon-plus" />
        </BlockHandleAdd>
        <BlockHandleDraggable>
          <div className="icon-drag" />
        </BlockHandleDraggable>
      </BlockHandlePopover>
      
      {/* Drop indicator */}
      <DropIndicator />
    </ProseKit>
  )
}

Components

BlockHandlePopover

The container that appears beside blocks on hover.
placement
string
default:"left"
Where to position the handle relative to the block. Options: left or right.
hoist
boolean
default:"false"
Whether to use the browser Popover API. Setting to true may cause a small delay when scrolling.
onStateChange
(state: { node: Node; pos: number } | null) => void
Callback when the hovered block changes. Receives the ProseMirror node and position, or null when not hovering.
className
string
CSS class name for styling.

BlockHandleAdd

Button that inserts a new block below the current one.
className
string
CSS class name for styling.

BlockHandleDraggable

Draggable handle for reordering blocks.
className
string
CSS class name for styling.

DropIndicator

Visual indicator showing where the dragged block will be dropped. Should be rendered once in your editor.
className
string
CSS class name for styling.

Complete Example

import { ProseKit } from 'prosekit/react'
import {
  BlockHandlePopover,
  BlockHandleAdd,
  BlockHandleDraggable,
} from 'prosekit/react/block-handle'
import { DropIndicator } from 'prosekit/react/drop-indicator'

interface BlockHandleProps {
  dir?: 'ltr' | 'rtl'
}

export function BlockHandle({ dir }: BlockHandleProps) {
  return (
    <BlockHandlePopover
      placement={dir === 'rtl' ? 'right' : 'left'}
      className="block-handle-popover"
    >
      <BlockHandleAdd className="block-handle-add">
        <svg width="16" height="16" viewBox="0 0 16 16">
          <path d="M8 3v10M3 8h10" stroke="currentColor" strokeWidth="2" />
        </svg>
      </BlockHandleAdd>
      
      <BlockHandleDraggable className="block-handle-drag">
        <svg width="16" height="16" viewBox="0 0 16 16">
          <circle cx="6" cy="4" r="1" fill="currentColor" />
          <circle cx="6" cy="8" r="1" fill="currentColor" />
          <circle cx="6" cy="12" r="1" fill="currentColor" />
          <circle cx="10" cy="4" r="1" fill="currentColor" />
          <circle cx="10" cy="8" r="1" fill="currentColor" />
          <circle cx="10" cy="12" r="1" fill="currentColor" />
        </svg>
      </BlockHandleDraggable>
    </BlockHandlePopover>
  )
}

export default function Editor() {
  const editor = useMemo(() => {
    return createEditor({ extension: defineExtension() })
  }, [])

  return (
    <ProseKit editor={editor}>
      <div className="editor-container">
        <div ref={editor.mount} className="editor-content" />
        <BlockHandle />
        <DropIndicator />
      </div>
    </ProseKit>
  )
}

Styling

.block-handle-popover {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 4px;
  background: white;
  border: 1px solid #e5e7eb;
  border-radius: 6px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.block-handle-add,
.block-handle-drag {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border: none;
  background: transparent;
  border-radius: 4px;
  color: #6b7280;
  cursor: pointer;
  transition: all 0.15s;
}

.block-handle-add:hover,
.block-handle-drag:hover {
  background: #f3f4f6;
  color: #111827;
}

.block-handle-drag {
  cursor: grab;
}

.block-handle-drag:active {
  cursor: grabbing;
}

/* Drop indicator */
.drop-indicator {
  height: 2px;
  background: #3b82f6;
  border-radius: 1px;
  position: absolute;
  pointer-events: none;
}

RTL Support

For right-to-left languages, position the handle on the right side:
<BlockHandlePopover placement="right">
  {/* ... */}
</BlockHandlePopover>

Behavior

Hover Activation

The block handle automatically appears when you hover over a block. The component detects:
  • Block boundaries
  • Hoverable blocks (paragraphs, headings, lists, etc.)
  • Mouse position relative to blocks

Drag and Drop

  1. Start drag: Click and hold the drag handle
  2. Show indicator: A blue line appears showing drop position
  3. Drop: Release to move the block
  4. Cancel: Press Escape to cancel the drag

Add Block

Clicking the add button:
  1. Inserts a new paragraph below the current block
  2. Moves focus to the new paragraph
  3. Maintains undo history

Accessibility

  • Keyboard accessible (drag with keyboard not yet supported)
  • ARIA labels included
  • Focus management
  • Screen reader announcements for drag operations

Advanced Usage

Custom Add Behavior

You can customize what happens when the add button is clicked by listening to the click event:
<BlockHandleAdd 
  onClick={(e) => {
    // Custom logic
    editor.commands.insertBlockBelow()
  }}
>
  +
</BlockHandleAdd>

Track Hovered Block

const [hoveredBlock, setHoveredBlock] = useState(null)

<BlockHandlePopover
  onStateChange={(state) => {
    setHoveredBlock(state)
    console.log('Hovered block:', state?.node.type.name, 'at position', state?.pos)
  }}
>
  {/* ... */}
</BlockHandlePopover>

Only Dragging (No Add Button)

<BlockHandlePopover>
  <BlockHandleDraggable>
    <DragIcon />
  </BlockHandleDraggable>
</BlockHandlePopover>

Only Add Button (No Dragging)

<BlockHandlePopover>
  <BlockHandleAdd>
    <PlusIcon />
  </BlockHandleAdd>
</BlockHandlePopover>

Common Issues

Handle doesn’t appear

  • Ensure BlockHandlePopover is rendered inside <ProseKit>
  • Check that your blocks have proper structure
  • Verify CSS isn’t hiding the handle

Drag doesn’t work

  • Make sure DropIndicator is rendered
  • Check that drag events aren’t being prevented
  • Verify the editor has drag-and-drop extension enabled

Performance issues

  • Avoid setting hoist={true} unless necessary
  • Optimize hover detection with CSS
  • Debounce hover events if needed

Best Practices

  1. Visual clarity: Use clear icons that users understand (⋮⋮ for drag, + for add)
  2. Consistent positioning: Keep handles on the same side throughout your app
  3. Mobile: Consider disabling drag-and-drop on mobile or using a different interaction pattern
  4. Feedback: Provide clear visual feedback during drag operations
  5. Performance: The default settings are optimized. Avoid hoist={true} unless you have z-index issues

See Also