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
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
When true, pauses hover tracking (useful when BlockOptions is open)
Additional CSS class name
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
};
Button content (usually an icon)
onClick
(event: MouseEvent) => void
Click handler
Tooltip and aria-label text
Whether the button is disabled
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