Documentation Index Fetch the complete documentation index at: https://docs.yoopta.dev/llms.txt
Use this file to discover all available pages before exploring further.
Overview
The Block DnD components provide drag-and-drop functionality for reordering blocks in your editor. Built on top of @dnd-kit , it integrates seamlessly with the renderBlock API to keep DnD logic outside the core editor.
Features
@dnd-kit powered — industry-standard drag and drop library
Multi-block selection — drag multiple selected blocks at once
Visual feedback — drop indicators and drag overlays
Accessible — keyboard support built-in
Customizable — style drag handles and drop indicators
Non-invasive — uses renderBlock API, no core editor changes
Installation
npm install @yoopta/ui
# or
yarn add @yoopta/ui
The @yoopta/ui package includes @dnd-kit as a dependency.
Quick Start
import { useMemo , useCallback } from 'react' ;
import { createYooptaEditor , YooptaEditor , type RenderBlockProps } from '@yoopta/editor' ;
import { BlockDndContext , SortableBlock , DragHandle } from '@yoopta/ui/block-dnd' ;
function MyEditor () {
const editor = useMemo (
() => createYooptaEditor ({ plugins , marks }),
[],
);
const renderBlock = useCallback (({ children , blockId } : RenderBlockProps ) => (
< SortableBlock id = { blockId } useDragHandle >
< DragHandle blockId = { blockId } >
< GripVertical size = { 16 } />
</ DragHandle >
{ children }
</ SortableBlock >
), []);
return (
< BlockDndContext editor = { editor } >
< YooptaEditor
editor = { editor }
onChange = { ( value ) => console . log ( value ) }
placeholder = "Type / to open menu..."
renderBlock = { renderBlock }
>
{ /* Other UI components */ }
</ YooptaEditor >
</ BlockDndContext >
);
}
How It Works
The Block DnD system consists of three main parts:
BlockDndContext — Wraps the editor, provides DnD context and handles block reordering
SortableBlock — Wrapper for each block that makes it sortable. When using DragHandle, set useDragHandle={true} to prevent the entire block from being draggable
DragHandle — The draggable trigger element. Must be used with SortableBlock that has useDragHandle={true}
The renderBlock prop on YooptaEditor allows you to wrap each block with DnD components without modifying the core editor.
Important : When using DragHandle, you must set useDragHandle={true} on SortableBlock. This ensures that drag listeners are only applied to the handle, not the entire block.
API Reference
BlockDndContext
The root provider that wraps your editor and handles drag events.
< BlockDndContext
editor = { editor }
onDragStart = { ( event ) => console . log ( 'Drag started' , event ) }
onDragEnd = { ( event ) => console . log ( 'Drag ended' , event ) }
>
< YooptaEditor ... />
</ BlockDndContext >
Props:
Prop Type Description editorYooEditorThe Yoopta editor instance childrenReactNodeEditor and other components onDragStart(event: DragStartEvent) => voidCalled when drag starts onDragEnd(event: DragEndEvent) => voidCalled when drag ends
SortableBlock
Wrapper component that makes a block sortable.
< SortableBlock
id = { blockId }
disabled = { isReadOnly }
className = "my-sortable-block"
>
{ children }
</ SortableBlock >
Props:
Prop Type Description idstringBlock ID (required) childrenReactNodeBlock content disabledbooleanDisable sorting for this block classNamestringCustom CSS classes useDragHandlebooleanIf true, listeners won’t be applied to the block (use DragHandle instead). Required when using DragHandle
DragHandle
The draggable trigger component. Users drag this to move blocks.
< DragHandle
blockId = { blockId }
className = "my-drag-handle"
>
< GripVertical size = { 16 } />
</ DragHandle >
Props:
Prop Type Description blockIdstring | nullBlock ID this handle controls childrenReactNodeHandle content (usually an icon) classNamestringCustom CSS classes onClick(e: MouseEvent) => voidCalled when handle is clicked (not dragged) asChildbooleanIf true, merges props and event handlers with the child element (useful for integrating with other button components)
useBlockDnd
Hook for accessing DnD state and utilities.
const {
activeId ,
activeBlock ,
isDragging ,
draggedIds ,
getOrderedBlockIds ,
} = useBlockDnd ({ editor });
Returns:
Property Type Description activeIdstring | nullCurrently dragged block ID activeBlockYooptaBlockData | nullCurrently dragged block data isDraggingbooleanWhether a drag is in progress draggedIdsstring[]All block IDs being dragged (for multi-select)
useBlockDndContext
Hook to access the DnD context value directly.
const { activeId , activeBlock , isDragging , draggedIds , editor } = useBlockDndContext ();
getOrderedBlockIds
Utility function to get block IDs sorted by their order.
import { getOrderedBlockIds } from '@yoopta/ui/block-dnd' ;
const orderedIds = getOrderedBlockIds ( editor );
// ['block-1', 'block-2', 'block-3', ...]
Examples
Basic Setup
import { useMemo , useCallback } from 'react' ;
import { createYooptaEditor , YooptaEditor , type RenderBlockProps } from '@yoopta/editor' ;
import { BlockDndContext , SortableBlock , DragHandle } from '@yoopta/ui/block-dnd' ;
import { GripVertical } from 'lucide-react' ;
function Editor () {
const editor = useMemo (() => createYooptaEditor ({
plugins: PLUGINS ,
marks: MARKS ,
}), []);
const renderBlock = useCallback (({ children , blockId } : RenderBlockProps ) => (
< SortableBlock id = { blockId } useDragHandle >
< div className = "flex items-start gap-2" >
< DragHandle blockId = { blockId } className = "opacity-0 group-hover:opacity-100" >
< GripVertical size = { 16 } className = "text-gray-400" />
</ DragHandle >
< div className = "flex-1" > { children } </ div >
</ div >
</ SortableBlock >
), []);
return (
< BlockDndContext editor = { editor } >
< YooptaEditor
editor = { editor }
onChange = { ( value ) => console . log ( value ) }
placeholder = "Type / to open menu..."
renderBlock = { renderBlock }
/>
</ BlockDndContext >
);
}
With FloatingBlockActions
Integrate DnD with the existing FloatingBlockActions component:
import { FloatingBlockActions } from '@yoopta/ui/floating-block-actions' ;
import { BlockDndContext , SortableBlock , DragHandle } from '@yoopta/ui/block-dnd' ;
function Editor () {
const editor = useMemo (
() => createYooptaEditor ({ plugins , marks }),
[],
);
const renderBlock = useCallback (({ children , blockId } : RenderBlockProps ) => (
< SortableBlock id = { blockId } useDragHandle >
{ children }
</ SortableBlock >
), []);
return (
< BlockDndContext editor = { editor } >
< YooptaEditor editor = { editor } onChange = { onChange } placeholder = "Type / to open menu..." renderBlock = { renderBlock } >
< FloatingBlockActions >
{ ({ blockId }) => (
<>
< FloatingBlockActions.Button onClick = { () => onPlusClick ( blockId ) } >
< PlusIcon />
</ FloatingBlockActions.Button >
{ /* Use DragHandle with asChild to integrate with FloatingBlockActions.Button */ }
< DragHandle blockId = { blockId } asChild >
< FloatingBlockActions.Button title = "Drag to reorder" >
< GripVertical size = { 16 } />
</ FloatingBlockActions.Button >
</ DragHandle >
</>
) }
</ FloatingBlockActions >
</ YooptaEditor >
</ BlockDndContext >
);
}
Multi-Block Drag
When blocks are selected (via Shift+Click or SelectionBox), dragging one will move all selected blocks:
function Editor () {
const editor = useMemo (
() => createYooptaEditor ({ plugins , marks }),
[],
);
const renderBlock = useCallback (({ children , blockId , block } : RenderBlockProps ) => {
// Check if this block is in the multi-selection
const selectedPaths = editor . path . selected ;
const isSelected = selectedPaths ?. includes ( block . meta . order );
return (
< SortableBlock id = { blockId } useDragHandle >
< div className = { isSelected ? 'ring-2 ring-blue-500' : '' } >
< DragHandle blockId = { blockId } >
< GripVertical size = { 16 } />
</ DragHandle >
{ children }
</ div >
</ SortableBlock >
);
}, [ editor ]);
return (
< BlockDndContext editor = { editor } >
< YooptaEditor editor = { editor } onChange = { onChange } renderBlock = { renderBlock } >
< SelectionBox />
</ YooptaEditor >
</ BlockDndContext >
);
}
Read-Only Mode
Disable DnD in read-only mode:
function Editor ({ readOnly }) {
const editor = useMemo (
() => createYooptaEditor ({ plugins , marks , readOnly }),
[ readOnly ],
);
const renderBlock = useCallback (({ children , blockId } : RenderBlockProps ) => (
< SortableBlock id = { blockId } disabled = { readOnly } useDragHandle = { ! readOnly } >
{ ! readOnly && (
< DragHandle blockId = { blockId } >
< GripVertical size = { 16 } />
</ DragHandle >
) }
{ children }
</ SortableBlock >
), [ readOnly ]);
return (
< BlockDndContext editor = { editor } >
< YooptaEditor editor = { editor } onChange = { onChange } renderBlock = { renderBlock } />
</ BlockDndContext >
);
}
Styling
CSS Variables
:root {
/* Drag handle */
--yoopta-block-dnd-handle-opacity : 0 ;
--yoopta-block-dnd-handle-hover-opacity : 1 ;
--yoopta-block-dnd-handle-color : var ( --yoopta-ui-muted-foreground );
--yoopta-block-dnd-focus-ring : #3b82f6 ;
/* Drop indicator */
--yoopta-block-dnd-indicator-color : #3b82f6 ;
--yoopta-block-dnd-indicator-height : 2 px ;
/* Dragging state */
--yoopta-block-dnd-dragging-opacity : 0.5 ;
--yoopta-block-dnd-dragging-bg : var ( --yoopta-ui-accent );
/* Drag overlay */
--yoopta-block-dnd-overlay-bg : #ffffff ;
--yoopta-block-dnd-overlay-border : #e5e7eb ;
--yoopta-block-dnd-overlay-text : #374151 ;
--yoopta-block-dnd-overlay-text-accent : #1f2937 ;
--yoopta-block-dnd-overlay-icon-color : #6b7280 ;
}
Custom Styles
< SortableBlock id = { blockId } className = "group relative" useDragHandle >
< DragHandle
blockId = { blockId }
className = "absolute -left-8 top-1 opacity-0 group-hover:opacity-100 transition-opacity"
>
< GripVertical className = "w-4 h-4 text-gray-400 hover:text-gray-600" />
</ DragHandle >
{ children }
</ SortableBlock >
The renderBlock API
The renderBlock prop on YooptaEditor is the key to integrating DnD without modifying the core editor.
export type RenderBlockProps = {
/** The block data */
block : YooptaBlockData ;
/** The rendered block content */
children : ReactNode ;
/** The block's unique ID */
blockId : string ;
/** The block's order index */
index : number ;
};
< YooptaEditor
editor = { editor }
onChange = { onChange }
placeholder = "Type / to open menu..."
renderBlock = { ({ children , blockId } : RenderBlockProps ) => (
< SortableBlock id = { blockId } >
{ children }
</ SortableBlock >
) }
/>
This pattern allows you to:
Add DnD support
Add custom block wrappers
Implement block-level features without forking the editor
Best Practices
Always wrap renderBlock in useCallback to prevent unnecessary re-renders: const renderBlock = useCallback (({ children , blockId } : RenderBlockProps ) => (
< SortableBlock id = { blockId } useDragHandle > { children } </ SortableBlock >
), []);
Place BlockDndContext outside YooptaEditor
The context must wrap the entire editor: // Correct
< BlockDndContext editor = { editor } >
< YooptaEditor editor = { editor } renderBlock = { renderBlock } >
{ /* UI components */ }
</ YooptaEditor >
</ BlockDndContext >
// Wrong - context inside editor
< YooptaEditor editor = { editor } >
< BlockDndContext editor = { editor } >
{ /* ... */ }
</ BlockDndContext >
</ YooptaEditor >
Disable drag handles and sorting in read-only mode: const isReadOnly = useYooptaReadOnly ();
< SortableBlock id = { blockId } disabled = { isReadOnly } useDragHandle = { ! isReadOnly } >
{ ! isReadOnly && < DragHandle blockId = { blockId } > ... </ DragHandle > }
{ children }
</ SortableBlock >
TypeScript
Full type definitions are exported:
import type {
BlockDndContextProps ,
BlockDndContextValue ,
SortableBlockData ,
SortableBlockProps ,
DragHandleProps ,
DropIndicatorProps ,
UseBlockDndOptions ,
UseBlockDndReturn ,
} from '@yoopta/ui/block-dnd' ;
FloatingBlockActions Combine with floating actions for a complete block UI
SelectionBox Multi-select blocks for batch operations