Skip to main content
The SelectionBox component renders a visual selection rectangle when dragging to select multiple blocks. It provides visual feedback during multi-block selection operations.

Installation

npm install @yoopta/ui

Basic Usage

import YooptaEditor, { createYooptaEditor } from '@yoopta/editor';
import { SelectionBox } from '@yoopta/ui';
import { useMemo, useRef } from 'react';

function MyEditor() {
  const editor = useMemo(() => createYooptaEditor({ plugins }), []);
  const containerRef = useRef<HTMLDivElement>(null);

  return (
    <div ref={containerRef}>
      <YooptaEditor editor={editor}>
        <SelectionBox selectionBoxElement={containerRef} />
      </YooptaEditor>
    </div>
  );
}

Component API

SelectionBox

selectionBoxElement
HTMLElement | { current: HTMLElement | null } | null
Container element to constrain selection box (typically the editor container)

How It Works

The selection box:
  1. Activates when user clicks and drags in the editor
  2. Renders a semi-transparent blue rectangle showing the selection area
  3. Updates position and size as the mouse moves
  4. Selects blocks that intersect with the rectangle
  5. Deactivates when the mouse button is released

Examples

Basic Setup

import { SelectionBox } from '@yoopta/ui';
import YooptaEditor from '@yoopta/editor';
import { useRef } from 'react';

function Editor() {
  const containerRef = useRef<HTMLDivElement>(null);

  return (
    <div ref={containerRef} className="editor-container">
      <YooptaEditor editor={editor}>
        <SelectionBox selectionBoxElement={containerRef} />
      </YooptaEditor>
    </div>
  );
}

Without Container Ref

If no container is provided, selection works on the entire document:
import { SelectionBox } from '@yoopta/ui';

function Editor() {
  return (
    <YooptaEditor editor={editor}>
      <SelectionBox />
    </YooptaEditor>
  );
}

With Custom Container

import { SelectionBox } from '@yoopta/ui';
import { useRef } from 'react';

function Editor() {
  const scrollContainerRef = useRef<HTMLDivElement>(null);

  return (
    <div 
      ref={scrollContainerRef}
      style={{ height: '600px', overflow: 'auto' }}
    >
      <YooptaEditor editor={editor}>
        <SelectionBox selectionBoxElement={scrollContainerRef} />
      </YooptaEditor>
    </div>
  );
}

With Drag and Drop

Combine with block drag and drop for a complete experience:
import { SelectionBox } from '@yoopta/ui';
import { BlockDndContext, SortableBlock } from '@yoopta/ui';
import { useRef, useCallback } from 'react';
import type { RenderBlockProps } from '@yoopta/editor';

function Editor() {
  const containerRef = useRef<HTMLDivElement>(null);

  const renderBlock = useCallback(
    ({ children, blockId }: RenderBlockProps) => (
      <SortableBlock id={blockId} useDragHandle>
        {children}
      </SortableBlock>
    ),
    []
  );

  return (
    <div ref={containerRef}>
      <BlockDndContext editor={editor}>
        <YooptaEditor editor={editor} renderBlock={renderBlock}>
          <SelectionBox selectionBoxElement={containerRef} />
        </YooptaEditor>
      </BlockDndContext>
    </div>
  );
}

useRectangeSelectionBox Hook

For advanced use cases, you can use the underlying hook:
import { useRectangeSelectionBox } from '@yoopta/ui';
import { useYooptaEditor } from '@yoopta/editor';
import { useRef } from 'react';
import type { RectangeSelectionProps } from '@yoopta/ui';

function CustomSelectionBox() {
  const editor = useYooptaEditor();
  const containerRef = useRef<HTMLDivElement>(null);
  
  const { coords, origin, selection } = useRectangeSelectionBox({
    editor,
    root: containerRef.current ?? undefined,
  });

  if (!selection) return null;

  return (
    <div
      style={{
        position: 'fixed',
        left: origin[0],
        top: origin[1],
        width: Math.abs(coords[0] - origin[0]),
        height: Math.abs(coords[1] - origin[1]),
        backgroundColor: 'rgba(0, 100, 255, 0.1)',
        border: '2px solid rgb(0, 100, 255)',
        pointerEvents: 'none',
      }}
    />
  );
}

Hook Return Type

type RectangeSelectionState = {
  origin: [number, number];  // Starting point (page coordinates)
  coords: [number, number];  // Current point (viewport coordinates)
  selection: boolean;        // Whether selection is active
};

Behavior

Selection Activation

Selection activates when:
  • User clicks in empty space (not on text or interactive elements)
  • Mouse is dragged while holding the button
  • Not in read-only mode

Selection Logic

Blocks are selected when:
  • The selection rectangle intersects with the block’s bounding box
  • Partial overlap is sufficient for selection
  • Selection state updates in real-time during drag

Deactivation

Selection deactivates when:
  • Mouse button is released
  • User presses Escape
  • Click occurs outside the editor

Coordinates

  • Origin: Stored in page coordinates (accounts for initial scroll position)
  • Coords: Stored in viewport coordinates (updates during scroll)
  • Position: Rendered using position: fixed for smooth scrolling

Styling

Default Styles

The selection box has these default styles:
{
  position: fixed;
  background-color: rgba(35, 131, 226, 0.14);
  border: 1px solid rgba(35, 131, 226, 0.4);
  pointer-events: none;
  z-index: 10;
}

Custom Styling

Create a custom selection box component:
import { useRectangeSelectionBox } from '@yoopta/ui';
import { useYooptaEditor, useYooptaReadOnly } from '@yoopta/editor';
import { useMemo } from 'react';

function CustomSelectionBox({ containerRef }) {
  const editor = useYooptaEditor();
  const isReadOnly = useYooptaReadOnly();
  const { coords, origin, selection } = useRectangeSelectionBox({
    editor,
    root: containerRef?.current,
  });

  const originViewport: [number, number] = useMemo(
    () => [
      origin[0] - window.scrollX,
      origin[1] - window.scrollY,
    ],
    [origin, coords]
  );

  if (!selection || isReadOnly) return null;

  return (
    <div
      style={{
        position: 'fixed',
        left: originViewport[0],
        top: originViewport[1],
        width: Math.abs(coords[0] - originViewport[0]),
        height: Math.abs(coords[1] - originViewport[1]),
        backgroundColor: 'rgba(255, 100, 0, 0.1)',
        border: '2px dashed rgb(255, 100, 0)',
        borderRadius: '4px',
        pointerEvents: 'none',
        zIndex: 100,
      }}
    />
  );
}

Read-Only Mode

The selection box is automatically hidden in read-only mode:
import { SelectionBox } from '@yoopta/ui';

function Editor() {
  return (
    <YooptaEditor editor={editor} readOnly={true}>
      {/* SelectionBox won't render when readOnly={true} */}
      <SelectionBox />
    </YooptaEditor>
  );
}

Multi-Block Operations

Once blocks are selected, you can perform bulk operations:
import { useYooptaEditor } from '@yoopta/editor';
import { useEffect } from 'react';

function useMultiBlockActions() {
  const editor = useYooptaEditor();

  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      const selectedPaths = editor.path.selected;
      
      if (!Array.isArray(selectedPaths) || selectedPaths.length === 0) {
        return;
      }

      // Delete selected blocks
      if (e.key === 'Backspace' || e.key === 'Delete') {
        e.preventDefault();
        editor.batchOperations(() => {
          selectedPaths.forEach((path) => {
            const blockId = Object.keys(editor.children).find(
              (id) => editor.children[id].meta.order === path
            );
            if (blockId) {
              editor.deleteBlock(blockId);
            }
          });
        });
      }

      // Copy selected blocks
      if ((e.metaKey || e.ctrlKey) && e.key === 'c') {
        // Copy logic
      }
    };

    document.addEventListener('keydown', handleKeyDown);
    return () => document.removeEventListener('keydown', handleKeyDown);
  }, [editor]);
}

TypeScript

import type {
  SelectionBoxProps,
  RectangeSelectionProps,
  RectangeSelectionState,
  SelectionBoxRoot,
} from '@yoopta/ui';

See Also

Build docs developers (and LLMs) love