Block Handle
Block handle components enable users to interact with, reorder, and insert blocks in your editor through drag-and-drop operations.
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { useMemo } from 'preact/hooks'
import { createEditor } from 'prosekit/core'
import { ProseKit } from 'prosekit/preact'
import BlockHandle from './block-handle'
import { DEFAULT_DRAG_AND_DROP_CONTENT } from './default-content-drag-and-drop'
import DropIndicator from './drop-indicator'
import { defineExtension } from './extension'
export default function Editor() {
const editor = useMemo(() => {
const extension = defineExtension()
return createEditor({ extension, defaultContent: DEFAULT_DRAG_AND_DROP_CONTENT })
}, [])
return (
<ProseKit editor={editor}>
<div className="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 color-black dark:color-white">
<div className="relative w-full flex-1 box-border overflow-y-scroll">
<div ref={editor.mount} className="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
<BlockHandle />
<DropIndicator />
</div>
</div>
</ProseKit>
)
}
import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
export function defineExtension() {
return union([defineBasicExtension()])
}
export type EditorExtension = ReturnType<typeof defineExtension>
import {
BlockHandleAdd,
BlockHandleDraggable,
BlockHandlePopover,
} from 'prosekit/preact/block-handle'
export default function BlockHandle() {
return (
<BlockHandlePopover className="flex items-center flex-row box-border justify-center transition border-0 [&:not([data-state])]:hidden will-change-transform data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95 data-[state=open]:animate-duration-150 data-[state=closed]:animate-duration-200">
<BlockHandleAdd className="flex items-center box-border justify-center h-[1.5em] w-[1.5em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-pointer">
<div className="i-lucide-plus size-5 block" />
</BlockHandleAdd>
<BlockHandleDraggable className="flex items-center box-border justify-center h-[1.5em] w-[1.2em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-grab">
<div className="i-lucide-grip-vertical size-5 block" />
</BlockHandleDraggable>
</BlockHandlePopover>
)
}
// This is the default content for drag and drop examples.
export const DEFAULT_DRAG_AND_DROP_CONTENT = `
<h1>Drag and Drop Demo</h1>
<p>Try dragging any paragraph or heading by clicking on the handle that appears on the left when you hover over it.</p>
<h2>Getting Started</h2>
<p>Hover over any block to see the drag handle appear. Click and drag to reorder content.</p>
<p>This paragraph can be moved above or below other blocks.</p>
<h2>Different Block Types</h2>
<p>You can drag paragraphs, headings, lists, code blocks, and more.</p>
<h3>Lists Work Too</h3>
<ul>
<li>This entire list can be dragged</li>
<li>Individual list items stay together</li>
<li>Try moving this list around</li>
</ul>
<ol>
<li>Ordered lists also support dragging</li>
<li>The numbering updates automatically</li>
<li>Drag this list to see it in action</li>
</ol>
<h3>Code Blocks</h3>
<p>Even code blocks can be moved:</p>
<pre data-language="javascript"><code>// This code block can be dragged
function dragAndDrop() {
return "Easy to rearrange!"
}</code></pre>
<h2>Nested Content</h2>
<blockquote>
<p>This blockquote can be moved as a single unit.</p>
<blockquote>
<p>Nested blockquotes move together with their parent.</p>
</blockquote>
</blockquote>
<h2>Try It Yourself</h2>
<p>Practice by moving this paragraph to the top of the document.</p>
<p>Or drag this one to between the headings above.</p>
<p>The drag handles make it easy to reorganize your content exactly how you want it.</p>
`
import { DropIndicator as BaseDropIndicator } from 'prosekit/preact/drop-indicator'
export default function DropIndicator() {
return <BaseDropIndicator className="z-50 transition-all bg-blue-500" />
}
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { createEditor } from 'prosekit/core'
import { ProseKit } from 'prosekit/react'
import { useMemo } from 'react'
import BlockHandle from './block-handle'
import { DEFAULT_DRAG_AND_DROP_CONTENT } from './default-content-drag-and-drop'
import DropIndicator from './drop-indicator'
import { defineExtension } from './extension'
export default function Editor() {
const editor = useMemo(() => {
const extension = defineExtension()
return createEditor({ extension, defaultContent: DEFAULT_DRAG_AND_DROP_CONTENT })
}, [])
return (
<ProseKit editor={editor}>
<div className="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 color-black dark:color-white">
<div className="relative w-full flex-1 box-border overflow-y-scroll">
<div ref={editor.mount} className="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
<BlockHandle />
<DropIndicator />
</div>
</div>
</ProseKit>
)
}
import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
export function defineExtension() {
return union([defineBasicExtension()])
}
export type EditorExtension = ReturnType<typeof defineExtension>
import {
BlockHandleAdd,
BlockHandleDraggable,
BlockHandlePopover,
} from 'prosekit/react/block-handle'
export default function BlockHandle() {
return (
<BlockHandlePopover className="flex items-center flex-row box-border justify-center transition border-0 [&:not([data-state])]:hidden will-change-transform data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95 data-[state=open]:animate-duration-150 data-[state=closed]:animate-duration-200">
<BlockHandleAdd className="flex items-center box-border justify-center h-[1.5em] w-[1.5em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-pointer">
<div className="i-lucide-plus size-5 block" />
</BlockHandleAdd>
<BlockHandleDraggable className="flex items-center box-border justify-center h-[1.5em] w-[1.2em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-grab">
<div className="i-lucide-grip-vertical size-5 block" />
</BlockHandleDraggable>
</BlockHandlePopover>
)
}
// This is the default content for drag and drop examples.
export const DEFAULT_DRAG_AND_DROP_CONTENT = `
<h1>Drag and Drop Demo</h1>
<p>Try dragging any paragraph or heading by clicking on the handle that appears on the left when you hover over it.</p>
<h2>Getting Started</h2>
<p>Hover over any block to see the drag handle appear. Click and drag to reorder content.</p>
<p>This paragraph can be moved above or below other blocks.</p>
<h2>Different Block Types</h2>
<p>You can drag paragraphs, headings, lists, code blocks, and more.</p>
<h3>Lists Work Too</h3>
<ul>
<li>This entire list can be dragged</li>
<li>Individual list items stay together</li>
<li>Try moving this list around</li>
</ul>
<ol>
<li>Ordered lists also support dragging</li>
<li>The numbering updates automatically</li>
<li>Drag this list to see it in action</li>
</ol>
<h3>Code Blocks</h3>
<p>Even code blocks can be moved:</p>
<pre data-language="javascript"><code>// This code block can be dragged
function dragAndDrop() {
return "Easy to rearrange!"
}</code></pre>
<h2>Nested Content</h2>
<blockquote>
<p>This blockquote can be moved as a single unit.</p>
<blockquote>
<p>Nested blockquotes move together with their parent.</p>
</blockquote>
</blockquote>
<h2>Try It Yourself</h2>
<p>Practice by moving this paragraph to the top of the document.</p>
<p>Or drag this one to between the headings above.</p>
<p>The drag handles make it easy to reorganize your content exactly how you want it.</p>
`
import { DropIndicator as BaseDropIndicator } from 'prosekit/react/drop-indicator'
export default function DropIndicator() {
return <BaseDropIndicator className="z-50 transition-all bg-blue-500" />
}
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { createEditor } from 'prosekit/core'
import { ProseKit } from 'prosekit/solid'
import BlockHandle from './block-handle'
import { DEFAULT_DRAG_AND_DROP_CONTENT } from './default-content-drag-and-drop'
import DropIndicator from './drop-indicator'
import { defineExtension } from './extension'
export default function Editor() {
const editor = createEditor({
extension: defineExtension(),
defaultContent: DEFAULT_DRAG_AND_DROP_CONTENT,
})
return (
<ProseKit editor={editor}>
<div class="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 color-black dark:color-white">
<div class="relative w-full flex-1 box-border overflow-y-scroll">
<div ref={editor.mount} class="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500" />
<BlockHandle />
<DropIndicator />
</div>
</div>
</ProseKit>
)
}
import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
export function defineExtension() {
return union([defineBasicExtension()])
}
export type EditorExtension = ReturnType<typeof defineExtension>
import {
BlockHandleAdd,
BlockHandleDraggable,
BlockHandlePopover,
} from 'prosekit/solid/block-handle'
export default function BlockHandle() {
return (
<BlockHandlePopover class="flex items-center flex-row box-border justify-center transition border-0 [&:not([data-state])]:hidden will-change-transform data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95 data-[state=open]:animate-duration-150 data-[state=closed]:animate-duration-200">
<BlockHandleAdd class="flex items-center box-border justify-center h-[1.5em] w-[1.5em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-pointer">
<div class="i-lucide-plus size-5 block" />
</BlockHandleAdd>
<BlockHandleDraggable class="flex items-center box-border justify-center h-[1.5em] w-[1.2em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-grab">
<div class="i-lucide-grip-vertical size-5 block" />
</BlockHandleDraggable>
</BlockHandlePopover>
)
}
// This is the default content for drag and drop examples.
export const DEFAULT_DRAG_AND_DROP_CONTENT = `
<h1>Drag and Drop Demo</h1>
<p>Try dragging any paragraph or heading by clicking on the handle that appears on the left when you hover over it.</p>
<h2>Getting Started</h2>
<p>Hover over any block to see the drag handle appear. Click and drag to reorder content.</p>
<p>This paragraph can be moved above or below other blocks.</p>
<h2>Different Block Types</h2>
<p>You can drag paragraphs, headings, lists, code blocks, and more.</p>
<h3>Lists Work Too</h3>
<ul>
<li>This entire list can be dragged</li>
<li>Individual list items stay together</li>
<li>Try moving this list around</li>
</ul>
<ol>
<li>Ordered lists also support dragging</li>
<li>The numbering updates automatically</li>
<li>Drag this list to see it in action</li>
</ol>
<h3>Code Blocks</h3>
<p>Even code blocks can be moved:</p>
<pre data-language="javascript"><code>// This code block can be dragged
function dragAndDrop() {
return "Easy to rearrange!"
}</code></pre>
<h2>Nested Content</h2>
<blockquote>
<p>This blockquote can be moved as a single unit.</p>
<blockquote>
<p>Nested blockquotes move together with their parent.</p>
</blockquote>
</blockquote>
<h2>Try It Yourself</h2>
<p>Practice by moving this paragraph to the top of the document.</p>
<p>Or drag this one to between the headings above.</p>
<p>The drag handles make it easy to reorganize your content exactly how you want it.</p>
`
import { DropIndicator as BaseDropIndicator } from 'prosekit/solid/drop-indicator'
export default function DropIndicator() {
return <BaseDropIndicator class="z-50 transition-all bg-blue-500" />
}
<script lang="ts">
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { createEditor } from 'prosekit/core'
import { ProseKit } from 'prosekit/svelte'
import BlockHandle from './block-handle.svelte'
import { DEFAULT_DRAG_AND_DROP_CONTENT } from './default-content-drag-and-drop'
import DropIndicator from './drop-indicator.svelte'
import { defineExtension } from './extension'
const editor = createEditor({ extension: defineExtension(), defaultContent: DEFAULT_DRAG_AND_DROP_CONTENT })
const mount = (element: HTMLElement) => {
editor.mount(element)
return { destroy: () => editor.unmount() }
}
</script>
<ProseKit {editor}>
<div class="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 color-black dark:color-white">
<div class="relative w-full flex-1 box-border overflow-y-scroll">
<div use:mount class="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
<BlockHandle />
<DropIndicator />
</div>
</div>
</ProseKit>
import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
export function defineExtension() {
return union([defineBasicExtension()])
}
export type EditorExtension = ReturnType<typeof defineExtension>
<script lang="ts">
import {
BlockHandleAdd,
BlockHandleDraggable,
BlockHandlePopover,
} from 'prosekit/svelte/block-handle'
</script>
<BlockHandlePopover class="flex items-center flex-row box-border justify-center transition border-0 [&:not([data-state])]:hidden will-change-transform data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95 data-[state=open]:animate-duration-150 data-[state=closed]:animate-duration-200">
<BlockHandleAdd class="flex items-center box-border justify-center h-[1.5em] w-[1.5em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-pointer">
<div class="i-lucide-plus size-5 block"></div>
</BlockHandleAdd>
<BlockHandleDraggable class="flex items-center box-border justify-center h-[1.5em] w-[1.2em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-grab">
<div class="i-lucide-grip-vertical size-5 block"></div>
</BlockHandleDraggable>
</BlockHandlePopover>
// This is the default content for drag and drop examples.
export const DEFAULT_DRAG_AND_DROP_CONTENT = `
<h1>Drag and Drop Demo</h1>
<p>Try dragging any paragraph or heading by clicking on the handle that appears on the left when you hover over it.</p>
<h2>Getting Started</h2>
<p>Hover over any block to see the drag handle appear. Click and drag to reorder content.</p>
<p>This paragraph can be moved above or below other blocks.</p>
<h2>Different Block Types</h2>
<p>You can drag paragraphs, headings, lists, code blocks, and more.</p>
<h3>Lists Work Too</h3>
<ul>
<li>This entire list can be dragged</li>
<li>Individual list items stay together</li>
<li>Try moving this list around</li>
</ul>
<ol>
<li>Ordered lists also support dragging</li>
<li>The numbering updates automatically</li>
<li>Drag this list to see it in action</li>
</ol>
<h3>Code Blocks</h3>
<p>Even code blocks can be moved:</p>
<pre data-language="javascript"><code>// This code block can be dragged
function dragAndDrop() {
return "Easy to rearrange!"
}</code></pre>
<h2>Nested Content</h2>
<blockquote>
<p>This blockquote can be moved as a single unit.</p>
<blockquote>
<p>Nested blockquotes move together with their parent.</p>
</blockquote>
</blockquote>
<h2>Try It Yourself</h2>
<p>Practice by moving this paragraph to the top of the document.</p>
<p>Or drag this one to between the headings above.</p>
<p>The drag handles make it easy to reorganize your content exactly how you want it.</p>
`
<script lang="ts">
import { DropIndicator } from 'prosekit/svelte/drop-indicator'
</script>
<DropIndicator class="z-50 transition-all bg-blue-500" />
<script setup lang="ts">
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { createEditor } from 'prosekit/core'
import { ProseKit } from 'prosekit/vue'
import {
ref,
watchPostEffect,
} from 'vue'
import BlockHandle from './block-handle.vue'
import { DEFAULT_DRAG_AND_DROP_CONTENT } from './default-content-drag-and-drop'
import DropIndicator from './drop-indicator.vue'
import { defineExtension } from './extension'
const editor = createEditor({
extension: defineExtension(),
defaultContent: DEFAULT_DRAG_AND_DROP_CONTENT,
})
const editorRef = ref<HTMLDivElement | null>(null)
watchPostEffect((onCleanup) => {
editor.mount(editorRef.value)
onCleanup(() => editor.unmount())
})
</script>
<template>
<ProseKit :editor="editor">
<div class="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 color-black dark:color-white">
<div class="relative w-full flex-1 box-border overflow-y-scroll">
<div
ref="editorRef"
spellcheck="false"
class="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"
/>
<BlockHandle />
<DropIndicator />
</div>
</div>
</ProseKit>
</template>
import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
export function defineExtension() {
return union([defineBasicExtension()])
}
export type EditorExtension = ReturnType<typeof defineExtension>
<script setup lang="ts">
import {
BlockHandleAdd,
BlockHandleDraggable,
BlockHandlePopover,
} from 'prosekit/vue/block-handle'
</script>
<template>
<BlockHandlePopover class="flex items-center flex-row box-border justify-center transition border-0 [&:not([data-state])]:hidden will-change-transform data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95 data-[state=open]:animate-duration-150 data-[state=closed]:animate-duration-200">
<BlockHandleAdd class="flex items-center box-border justify-center h-[1.5em] w-[1.5em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-pointer">
<div class="i-lucide-plus size-5 block" />
</BlockHandleAdd>
<BlockHandleDraggable class="flex items-center box-border justify-center h-[1.5em] w-[1.2em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-grab">
<div class="i-lucide-grip-vertical size-5 block" />
</BlockHandleDraggable>
</BlockHandlePopover>
</template>
// This is the default content for drag and drop examples.
export const DEFAULT_DRAG_AND_DROP_CONTENT = `
<h1>Drag and Drop Demo</h1>
<p>Try dragging any paragraph or heading by clicking on the handle that appears on the left when you hover over it.</p>
<h2>Getting Started</h2>
<p>Hover over any block to see the drag handle appear. Click and drag to reorder content.</p>
<p>This paragraph can be moved above or below other blocks.</p>
<h2>Different Block Types</h2>
<p>You can drag paragraphs, headings, lists, code blocks, and more.</p>
<h3>Lists Work Too</h3>
<ul>
<li>This entire list can be dragged</li>
<li>Individual list items stay together</li>
<li>Try moving this list around</li>
</ul>
<ol>
<li>Ordered lists also support dragging</li>
<li>The numbering updates automatically</li>
<li>Drag this list to see it in action</li>
</ol>
<h3>Code Blocks</h3>
<p>Even code blocks can be moved:</p>
<pre data-language="javascript"><code>// This code block can be dragged
function dragAndDrop() {
return "Easy to rearrange!"
}</code></pre>
<h2>Nested Content</h2>
<blockquote>
<p>This blockquote can be moved as a single unit.</p>
<blockquote>
<p>Nested blockquotes move together with their parent.</p>
</blockquote>
</blockquote>
<h2>Try It Yourself</h2>
<p>Practice by moving this paragraph to the top of the document.</p>
<p>Or drag this one to between the headings above.</p>
<p>The drag handles make it easy to reorganize your content exactly how you want it.</p>
`
<script setup lang="ts">
import { DropIndicator } from 'prosekit/vue/drop-indicator'
</script>
<template>
<DropIndicator class-name="z-50 transition-all bg-blue-500" />
</template>
Overview
Section titled “Overview”The block handle system provides two main capabilities:
- Block manipulation: Add new blocks or drag existing blocks to reorder content
- Visual feedback: Show users exactly where content will be placed during drag operations
Components
Section titled “Components”Block Handle
Section titled “Block Handle”The BlockHandle
consists of three main components:
BlockHandlePopover
: A popover container that appears on the left side when hovering over a blockBlockHandleAdd
: An add button (+) that inserts a new block below the current oneBlockHandleDraggable
: A drag handle (⋮⋮) that allows users to reorder blocks by dragging
You can use BlockHandleAdd
, BlockHandleDraggable
, or both together depending on your needs.
Drop Indicator
Section titled “Drop Indicator”The DropIndicator
provides visual feedback during drag-and-drop operations by showing a horizontal line where the dragged content will be inserted. It automatically appears when dragging and disappears when the drag operation completes.
Installation
Section titled “Installation”Copy and paste the component source files linked above into your project.
API Reference
Section titled “API Reference”- block-handle
- drop-indicator