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.
Where to position the handle relative to the block. Options: left or right.
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.
CSS class name for styling.
BlockHandleAdd
Button that inserts a new block below the current one.
CSS class name for styling.
BlockHandleDraggable
Draggable handle for reordering blocks.
CSS class name for styling.
DropIndicator
Visual indicator showing where the dragged block will be dropped. Should be rendered once in your editor.
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
- Start drag: Click and hold the drag handle
- Show indicator: A blue line appears showing drop position
- Drop: Release to move the block
- Cancel: Press Escape to cancel the drag
Add Block
Clicking the add button:
- Inserts a new paragraph below the current block
- Moves focus to the new paragraph
- 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>
<BlockHandlePopover>
<BlockHandleDraggable>
<DragIcon />
</BlockHandleDraggable>
</BlockHandlePopover>
<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
- Avoid setting
hoist={true} unless necessary
- Optimize hover detection with CSS
- Debounce hover events if needed
Best Practices
-
Visual clarity: Use clear icons that users understand (⋮⋮ for drag, + for add)
-
Consistent positioning: Keep handles on the same side throughout your app
-
Mobile: Consider disabling drag-and-drop on mobile or using a different interaction pattern
-
Feedback: Provide clear visual feedback during drag operations
-
Performance: The default settings are optimized. Avoid
hoist={true} unless you have z-index issues
See Also