Skip to main content
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.
editor
YooEditor
required
The Yoopta editor instance
children
ReactNode
required
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
enableMultiDrag
boolean
default:"true"
Enable dragging multiple selected blocks

SortableBlock

Wrapper component that makes a block draggable and droppable.
id
string
required
Unique block ID
children
ReactNode
required
Block content
useDragHandle
boolean
default:"true"
If true, requires DragHandle component to initiate drag
disabled
boolean
default:"false"
Whether dragging is disabled for this block
className
string
Additional CSS class name

DragHandle

Button component that initiates drag when useDragHandle={true}.
blockId
string | null
required
Block ID this handle controls
children
ReactNode
required
Handle content (typically an icon)
asChild
boolean
default:"false"
Merge props with child element instead of rendering a button
onClick
(e: MouseEvent) => void
Called when handle is clicked (not dragged)
className
string
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

blockId
string | null
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:
  1. Dragging any selected block drags all selected blocks
  2. Blocks maintain relative order when moved
  3. 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)

Scroll Behavior

  • 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';

Performance

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

Build docs developers (and LLMs) love