Skip to main content
The FloatingBlockActions component displays a set of action buttons that appear when hovering over blocks. Typically used for adding blocks, showing block options, and drag handles.

Installation

npm install @yoopta/ui

Basic Usage

import { FloatingBlockActions } from '@yoopta/ui';
import { PlusIcon, GripVerticalIcon } from 'lucide-react';
import { useYooptaEditor, Blocks } from '@yoopta/editor';

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

  const onPlusClick = (blockId: string | null) => {
    if (!blockId) return;
    const block = Blocks.getBlock(editor, { id: blockId });
    if (!block) return;
    
    const nextOrder = block.meta.order + 1;
    editor.insertBlock('Paragraph', { at: nextOrder, focus: true });
  };

  return (
    <FloatingBlockActions>
      {({ blockId }) => (
        <>
          <FloatingBlockActions.Button
            onClick={() => onPlusClick(blockId)}
            title="Add block"
          >
            <PlusIcon size={16} />
          </FloatingBlockActions.Button>
          <FloatingBlockActions.Button title="Drag to reorder">
            <GripVerticalIcon size={16} />
          </FloatingBlockActions.Button>
        </>
      )}
    </FloatingBlockActions>
  );
}

Component API

FloatingBlockActions (Root)

The root component that tracks mouse position and manages action visibility.
children
ReactNode | ((api: FloatingBlockActionsApi) => ReactNode)
required
Action buttons or render function receiving block state
frozen
boolean
default:"false"
When true, pauses hover tracking (useful when BlockOptions is open)
className
string
Additional CSS class name
style
CSSProperties
Additional inline styles

FloatingBlockActionsApi

type FloatingBlockActionsApi = {
  blockId: string | null;           // Currently hovered block ID
  blockData: YooptaBlockData | null; // Block data for hovered block
  isVisible: boolean;                // Whether actions are visible
  hide: () => void;                  // Hide the floating actions manually
};

FloatingBlockActions.Button

children
ReactNode
required
Button content (usually an icon)
onClick
(event: MouseEvent) => void
Click handler
title
string
Tooltip and aria-label text
disabled
boolean
Whether the button is disabled
className
string
Additional CSS class name

Examples

With Block Options

import { FloatingBlockActions } from '@yoopta/ui';
import { BlockOptions } from '@yoopta/ui';
import { DragHandle } from '@yoopta/ui';
import { PlusIcon, GripVerticalIcon } from 'lucide-react';
import { useState, useRef } from 'react';
import { Blocks, useYooptaEditor } from '@yoopta/editor';

function ActionsWithOptions() {
  const editor = useYooptaEditor();
  const dragHandleRef = useRef<HTMLButtonElement>(null);
  const [blockOptionsOpen, setBlockOptionsOpen] = useState(false);

  const onPlusClick = (blockId: string | null) => {
    if (!blockId) return;
    const block = Blocks.getBlock(editor, { id: blockId });
    if (!block) return;
    
    editor.insertBlock('Paragraph', { at: block.meta.order + 1, focus: true });
  };

  const onDragClick = (blockId: string | null) => {
    if (!blockId) return;
    const block = Blocks.getBlock(editor, { id: blockId });
    if (!block) return;
    
    editor.setPath({ current: block.meta.order });
    setBlockOptionsOpen(true);
  };

  return (
    <FloatingBlockActions frozen={blockOptionsOpen}>
      {({ blockId }) => (
        <>
          <FloatingBlockActions.Button
            onClick={() => onPlusClick(blockId)}
            title="Add block"
          >
            <PlusIcon size={16} />
          </FloatingBlockActions.Button>
          
          <DragHandle blockId={blockId} ref={dragHandleRef} asChild>
            <FloatingBlockActions.Button
              onClick={() => onDragClick(blockId)}
              title="Drag to reorder"
            >
              <GripVerticalIcon size={16} />
            </FloatingBlockActions.Button>
          </DragHandle>

          <BlockOptions
            open={blockOptionsOpen}
            onOpenChange={setBlockOptionsOpen}
            blockId={blockId}
            anchor={dragHandleRef.current}
          />
        </>
      )}
    </FloatingBlockActions>
  );
}

Custom Actions

import { FloatingBlockActions } from '@yoopta/ui';
import { useYooptaEditor, Blocks } from '@yoopta/editor';
import { Copy, Trash2, MoveUp, MoveDown } from 'lucide-react';

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

  const duplicateBlock = (blockId: string | null) => {
    if (!blockId) return;
    editor.duplicateBlock(blockId);
  };

  const deleteBlock = (blockId: string | null) => {
    if (!blockId) return;
    editor.deleteBlock(blockId);
  };

  const moveUp = (blockId: string | null) => {
    if (!blockId) return;
    const block = Blocks.getBlock(editor, { id: blockId });
    if (!block || block.meta.order === 0) return;
    editor.moveBlock(blockId, block.meta.order - 1);
  };

  const moveDown = (blockId: string | null) => {
    if (!blockId) return;
    const block = Blocks.getBlock(editor, { id: blockId });
    if (!block) return;
    editor.moveBlock(blockId, block.meta.order + 1);
  };

  return (
    <FloatingBlockActions>
      {({ blockId }) => (
        <>
          <FloatingBlockActions.Button
            onClick={() => moveUp(blockId)}
            title="Move up"
          >
            <MoveUp size={16} />
          </FloatingBlockActions.Button>
          <FloatingBlockActions.Button
            onClick={() => moveDown(blockId)}
            title="Move down"
          >
            <MoveDown size={16} />
          </FloatingBlockActions.Button>
          <FloatingBlockActions.Button
            onClick={() => duplicateBlock(blockId)}
            title="Duplicate"
          >
            <Copy size={16} />
          </FloatingBlockActions.Button>
          <FloatingBlockActions.Button
            onClick={() => deleteBlock(blockId)}
            title="Delete"
          >
            <Trash2 size={16} />
          </FloatingBlockActions.Button>
        </>
      )}
    </FloatingBlockActions>
  );
}

Conditional Actions

import { FloatingBlockActions } from '@yoopta/ui';
import { PlusIcon, Settings } from 'lucide-react';

function ConditionalActions() {
  return (
    <FloatingBlockActions>
      {({ blockId, blockData, isVisible }) => (
        <>
          {isVisible && (
            <FloatingBlockActions.Button title="Add block">
              <PlusIcon size={16} />
            </FloatingBlockActions.Button>
          )}
          
          {/* Show settings only for certain block types */}
          {blockData?.type === 'Image' && (
            <FloatingBlockActions.Button title="Image settings">
              <Settings size={16} />
            </FloatingBlockActions.Button>
          )}
        </>
      )}
    </FloatingBlockActions>
  );
}

Programmatic Hide

import { FloatingBlockActions } from '@yoopta/ui';
import { useState } from 'react';

function ActionsWithHide() {
  const [customMenuOpen, setCustomMenuOpen] = useState(false);

  return (
    <FloatingBlockActions>
      {({ blockId, hide }) => (
        <>
          <FloatingBlockActions.Button
            onClick={() => {
              setCustomMenuOpen(true);
              hide(); // Hide floating actions
            }}
            title="Open menu"
          >
            •••
          </FloatingBlockActions.Button>
        </>
      )}
    </FloatingBlockActions>
  );
}

Behavior

Hover Tracking

The component automatically:
  • Detects closest block to mouse cursor
  • Shows actions to the left of the block
  • Updates position on mouse move
  • Hides on scroll
  • Respects frozen state
  • Provides hover bridge to prevent flicker

Positioning

Actions are positioned:
  • To the left of the block
  • Vertically centered with block content
  • Accounting for margin collapse
  • Within 100px vertical distance from cursor

Frozen State

When frozen={true}:
  • Hover tracking is paused
  • Current position is maintained
  • Actions remain visible
  • Useful when opening popovers/menus

Integration with Drag and Drop

See the Drag and Drop documentation for combining with DragHandle:
import { FloatingBlockActions } from '@yoopta/ui';
import { DragHandle } from '@yoopta/ui';
import { GripVerticalIcon } from 'lucide-react';

function ActionsWithDragHandle() {
  return (
    <FloatingBlockActions>
      {({ blockId }) => (
        <DragHandle blockId={blockId} asChild>
          <FloatingBlockActions.Button title="Drag to reorder">
            <GripVerticalIcon size={16} />
          </FloatingBlockActions.Button>
        </DragHandle>
      )}
    </FloatingBlockActions>
  );
}

Styling

CSS Classes

.yoopta-ui-floating-block-actions {
  /* Actions container */
}

.yoopta-ui-floating-action-button {
  /* Individual button */
}

.yoopta-ui-floating-action-button:hover {
  /* Button hover state */
}

.yoopta-ui-floating-action-button:disabled {
  /* Disabled button */
}

Custom Styling

<FloatingBlockActions 
  className="my-actions"
  style={{ gap: '4px' }}
>
  {({ blockId }) => (
    <FloatingBlockActions.Button className="my-button">
      {/* ... */}
    </FloatingBlockActions.Button>
  )}
</FloatingBlockActions>

TypeScript

import type {
  FloatingBlockActionsRootProps,
  FloatingBlockActionsButtonProps,
  FloatingBlockActionsApi,
} from '@yoopta/ui';

See Also

Build docs developers (and LLMs) love