Yoopta Editor provides drag and drop functionality for reordering blocks through the @yoopta/ui package. The system is built on @dnd-kit and provides components for making blocks sortable and adding drag handles.
Installation
npm install @yoopta/ui @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities
Basic Usage
import YooptaEditor, { createYooptaEditor } from '@yoopta/editor';
import { BlockDndContext, SortableBlock } from '@yoopta/ui';
import { useMemo, useCallback } from 'react';
import type { RenderBlockProps } from '@yoopta/editor';
function MyEditor() {
const editor = useMemo(() => createYooptaEditor({ plugins }), []);
const renderBlock = useCallback(
({ children, blockId }: RenderBlockProps) => (
<SortableBlock id={blockId} useDragHandle>
{children}
</SortableBlock>
),
[]
);
return (
<BlockDndContext editor={editor}>
<YooptaEditor editor={editor} renderBlock={renderBlock}>
{/* Your UI components */}
</YooptaEditor>
</BlockDndContext>
);
}
Core Components
BlockDndContext
Provider component that sets up drag and drop functionality.
The Yoopta editor instance
Editor and other components
onDragStart
(event: DragStartEvent, blocks: YooptaBlockData[]) => void
Called when drag starts
onDragEnd
(event: DragEndEvent, moved: boolean) => void
Called when drag ends
renderDragOverlay
(blocks: YooptaBlockData[]) => ReactNode
Custom drag overlay render function
Enable dragging multiple selected blocks
SortableBlock
Wrapper component that makes a block draggable and droppable.
If true, requires DragHandle component to initiate drag
Whether dragging is disabled for this block
Additional CSS class name
DragHandle
Button component that initiates drag when useDragHandle={true}.
Block ID this handle controls
Handle content (typically an icon)
Merge props with child element instead of rendering a button
Called when handle is clicked (not dragged)
Additional CSS class name
Examples
Complete Setup
import YooptaEditor, { createYooptaEditor } from '@yoopta/editor';
import {
BlockDndContext,
SortableBlock,
DragHandle,
FloatingBlockActions
} from '@yoopta/ui';
import { GripVertical, Plus } from 'lucide-react';
import { useMemo, useCallback } from 'react';
function Editor() {
const editor = useMemo(() => createYooptaEditor({ plugins }), []);
const renderBlock = useCallback(
({ children, blockId }: RenderBlockProps) => (
<SortableBlock id={blockId} useDragHandle>
{children}
</SortableBlock>
),
[]
);
return (
<BlockDndContext editor={editor}>
<YooptaEditor editor={editor} renderBlock={renderBlock}>
<FloatingBlockActions>
{({ blockId }) => (
<>
<FloatingBlockActions.Button title="Add block">
<Plus size={16} />
</FloatingBlockActions.Button>
<DragHandle blockId={blockId} asChild>
<FloatingBlockActions.Button title="Drag to reorder">
<GripVertical size={16} />
</FloatingBlockActions.Button>
</DragHandle>
</>
)}
</FloatingBlockActions>
</YooptaEditor>
</BlockDndContext>
);
}
Without Drag Handle
Drag from anywhere on the block:
import { SortableBlock } from '@yoopta/ui';
const renderBlock = useCallback(
({ children, blockId }: RenderBlockProps) => (
<SortableBlock id={blockId} useDragHandle={false}>
{children}
</SortableBlock>
),
[]
);
Custom Drag Events
import { BlockDndContext } from '@yoopta/ui';
import type { DragEndEvent, DragStartEvent } from '@dnd-kit/core';
import type { YooptaBlockData } from '@yoopta/editor';
function Editor() {
const handleDragStart = (
event: DragStartEvent,
blocks: YooptaBlockData[]
) => {
console.log('Dragging blocks:', blocks.map(b => b.type));
// Show custom UI, analytics, etc.
};
const handleDragEnd = (
event: DragEndEvent,
moved: boolean
) => {
if (moved) {
console.log('Blocks reordered');
// Save to backend, analytics, etc.
}
};
return (
<BlockDndContext
editor={editor}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
{/* ... */}
</BlockDndContext>
);
}
Custom Drag Overlay
import { BlockDndContext } from '@yoopta/ui';
import type { YooptaBlockData } from '@yoopta/editor';
function Editor() {
const renderDragOverlay = (blocks: YooptaBlockData[]) => (
<div style={{
padding: '12px',
backgroundColor: 'white',
border: '2px solid blue',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
}}>
<strong>Dragging {blocks.length} block(s)</strong>
<div style={{ fontSize: '14px', color: '#666' }}>
{blocks.map(b => b.type).join(', ')}
</div>
</div>
);
return (
<BlockDndContext
editor={editor}
renderDragOverlay={renderDragOverlay}
>
{/* ... */}
</BlockDndContext>
);
}
Conditional Dragging
import { SortableBlock } from '@yoopta/ui';
import type { RenderBlockProps } from '@yoopta/editor';
const renderBlock = useCallback(
({ children, blockId, block }: RenderBlockProps) => {
// Disable drag for certain block types
const isDraggable = block.type !== 'Hero' && block.type !== 'Footer';
return (
<SortableBlock
id={blockId}
useDragHandle
disabled={!isDraggable}
>
{children}
</SortableBlock>
);
},
[]
);
Multi-Block Drag
import { BlockDndContext, SortableBlock, SelectionBox } from '@yoopta/ui';
import { useRef } from 'react';
function Editor() {
const containerRef = useRef<HTMLDivElement>(null);
return (
<div ref={containerRef}>
<BlockDndContext editor={editor} enableMultiDrag={true}>
<YooptaEditor editor={editor} renderBlock={renderBlock}>
{/* Enable multi-select with SelectionBox */}
<SelectionBox selectionBoxElement={containerRef} />
</YooptaEditor>
</BlockDndContext>
</div>
);
}
useBlockDnd Hook
Access drag and drop state and utilities:
import { useBlockDnd } from '@yoopta/ui';
function MyComponent({ blockId }) {
const {
isDragging, // Any block being dragged
isBlockDragging, // This specific block being dragged
isPartOfMultiDrag, // This block part of multi-drag
dragCount, // Number of blocks being dragged
draggedIds, // IDs of dragged blocks
moveBlock, // (blockId, toIndex) => void
moveBlocks, // (blockIds, toIndex) => void
} = useBlockDnd({ blockId });
return (
<div>
{isDragging && <div>Drag in progress...</div>}
{isBlockDragging && <div>This block is being dragged</div>}
{isPartOfMultiDrag && (
<div>Part of {dragCount} block drag</div>
)}
</div>
);
}
Hook Options
Block ID to get drag state for
Hook Return Value
type UseBlockDndReturn = {
isDragging: boolean; // Any drag in progress
isBlockDragging: boolean; // This block being dragged
isPartOfMultiDrag: boolean; // Part of multi-block drag
dragCount: number; // Number of blocks dragged
draggedIds: string[]; // IDs of dragged blocks
moveBlock: (id: string, toIndex: number) => void;
moveBlocks: (ids: string[], toIndex: number) => void;
};
Helper Functions
getOrderedBlockIds
Get block IDs sorted by their order:
import { getOrderedBlockIds } from '@yoopta/ui';
import { useYooptaEditor } from '@yoopta/editor';
function MyComponent() {
const editor = useYooptaEditor();
const orderedIds = getOrderedBlockIds(editor);
return (
<ul>
{orderedIds.map((id) => (
<li key={id}>{editor.children[id].type}</li>
))}
</ul>
);
}
Behavior
Drag Detection
- With handle: Drag only initiates from
DragHandle component
- Without handle: Drag initiates from anywhere on the block
- Click vs drag: Detects if user is clicking or dragging
Multi-Block Drag
When enableMultiDrag={true} and blocks are selected:
- Dragging any selected block drags all selected blocks
- Blocks maintain relative order when moved
- Drop indicator shows where all blocks will be placed
Drop Indicators
Automatic visual indicators show:
- Where the block(s) will be dropped
- Valid drop zones
- Invalid drop zones (when disabled)
- Auto-scrolls when dragging near viewport edges
- Maintains smooth drag experience during scroll
- Updates drop zones during scroll
Styling
CSS Classes
/* Sortable block wrapper */
.yoopta-ui-block-dnd-sortable {}
/* Block being dragged */
.yoopta-ui-block-dnd-sortable--dragging {
opacity: 0.5;
}
/* Block being hovered over */
.yoopta-ui-block-dnd-sortable--over {}
/* Part of multi-drag */
.yoopta-ui-block-dnd-sortable--multi {}
/* Drop indicator */
.yoopta-ui-block-dnd-drop-indicator {
height: 2px;
background: blue;
}
/* Drag handle */
.yoopta-ui-block-dnd-handle {}
/* Handle being held */
.yoopta-ui-block-dnd-handle--holding {}
/* Active drag */
.yoopta-ui-block-dnd-handle--dragging {}
Custom Styling
<SortableBlock
id={blockId}
className="my-sortable-block"
>
{children}
</SortableBlock>
<DragHandle
blockId={blockId}
className="my-drag-handle"
>
<GripVertical />
</DragHandle>
Accessibility
- Drag handle has proper
aria-label
- Keyboard navigation support (Space/Enter to pick up, arrows to move)
- Screen reader announcements for drag operations
- Focus management during drag
TypeScript
import type {
BlockDndContextProps,
BlockDndContextValue,
SortableBlockProps,
DragHandleProps,
SortableBlockData,
UseBlockDndOptions,
UseBlockDndReturn,
} from '@yoopta/ui';
For large documents:
import { memo, useCallback } from 'react';
import { SortableBlock } from '@yoopta/ui';
const MemoizedBlock = memo(SortableBlock);
const renderBlock = useCallback(
({ children, blockId }: RenderBlockProps) => (
<MemoizedBlock id={blockId} useDragHandle>
{children}
</MemoizedBlock>
),
[]
);
See Also