Skip to main content

Overview

BlockOptions is a controlled context menu that appears next to a block and exposes actions such as duplicate, copy link, delete, or custom actions like “Turn into”. It uses Floating UI for smart positioning, a built-in overlay + portal, and the compound component pattern so you can structure the menu exactly the way you want.
Block options menu example

Features

  • Controlled API — open/close the menu programmatically from anywhere
  • Floating positioning — automatically positions relative to a reference element
  • Overlay + Portal — handles focus trapping, scroll locking, dismiss on outside click
  • Compound components — compose groups, buttons, separators however you like
  • Helper actions — duplicate, copy block link, delete helpers included
  • TypeScript-first — full typings for components and hooks

Installation

npm install @yoopta/ui
# or
yarn add @yoopta/ui

Basic Usage

import {
  BlockOptions,
  useBlockOptions,
  useBlockOptionsActions,
  useFloatingBlockActionsActions,
} from '@yoopta/ui';
import { CopyIcon, Trash2Icon } from 'lucide-react';

function MyBlockOptions() {
  const { isOpen, getRootProps, close, duplicateBlock, copyBlockLink, deleteBlock } =
    useBlockOptions();
  const { floatingBlockId, toggle: toggleFloating } = useFloatingBlockActionsActions();

  if (!isOpen) return null;

  const handleDuplicate = () => {
    if (!floatingBlockId) return;
    duplicateBlock(floatingBlockId);
  };

  const handleDelete = () => {
    if (!floatingBlockId) return;
    deleteBlock(floatingBlockId);
    toggleFloating('hovering');
  };

  return (
    <BlockOptions.Root {...getRootProps()} onClose={close}>
      <BlockOptions.Group>
        <BlockOptions.Button onClick={handleDuplicate} icon={<CopyIcon size={16} />}>
          Duplicate
        </BlockOptions.Button>
        <BlockOptions.Button
          onClick={() => floatingBlockId && copyBlockLink(floatingBlockId)}
          icon={<CopyIcon size={16} />}>
          Copy link
        </BlockOptions.Button>
        <BlockOptions.Button
          onClick={handleDelete}
          icon={<Trash2Icon size={16} />}
          variant="destructive">
          Delete
        </BlockOptions.Button>
      </BlockOptions.Group>
    </BlockOptions.Root>
  );
}

Controlled Architecture

BlockOptions follows the two-hook pattern:
HookPurposeUse it when…
useBlockOptions()Full hook with Floating UI for the component that renders <BlockOptions.Root>Rendering the menu
useBlockOptionsActions()Lightweight hook with only store actions + helpersOpening/closing menu from other components
// Component that renders the menu
const { isOpen, getRootProps } = useBlockOptions();

// Anywhere else (e.g., floating actions, toolbar)
const { open } = useBlockOptionsActions();

open({ reference: element, blockId: 'block-123' });

API Reference

Components

BlockOptions.Root

Root container rendered inside a portal + overlay.
<BlockOptions.Root {...getRootProps()}>{/* … */}</BlockOptions.Root>
Props
  • children: ReactNode
  • className?: string
  • style?: CSSProperties
  • onClose?: () => void — called when clicking outside/overlay

BlockOptions.Group

Groups buttons together (stacks vertically).
<BlockOptions.Group>
  <BlockOptions.Button>Duplicate</BlockOptions.Button>
</BlockOptions.Group>
Props: children, className?

BlockOptions.Button

Action button.
<BlockOptions.Button
  onClick={handleAction}
  icon={<PlusIcon size={14} />}
  variant="default"
  disabled={false}
  title="Duplicate block">
  Duplicate
</BlockOptions.Button>
Props
  • onClick?: (event) => void
  • icon?: ReactNode
  • variant?: 'default' | 'destructive'
  • disabled?: boolean
  • title?: string
  • className?: string
  • Inherits all native <button> attributes (type="button" by default)

BlockOptions.Separator

Visual separator between groups.
<BlockOptions.Separator />
Props: className?

Hooks

useBlockOptions()

Full hook for the component that renders the menu.
const { isOpen, getRootProps, open, close, duplicateBlock, copyBlockLink, deleteBlock } =
  useBlockOptions();
Returns
PropertyTypeDescription
isOpenbooleanWhether the menu is currently mounted (with transitions)
getRootProps() => RootPropsProps for <BlockOptions.Root> (floating ref, styles, event handlers)
open({ reference, blockId }) => voidOpen the menu with reference element + block ID
close() => voidClose the menu
duplicateBlock(blockId: string) => voidHelper action
copyBlockLink(blockId: string) => voidHelper action
deleteBlock(blockId: string) => voidHelper action

useBlockOptionsActions()

Lightweight hook with store actions (no Floating UI).
const { open, close, state, blockId, reference, duplicateBlock, copyBlockLink, deleteBlock } =
  useBlockOptionsActions();
Open options
open({
  reference: HTMLElement;  // Required - element to anchor the menu
  blockId?: string;        // Optional, but required for helper actions
});

Examples

1. Turn Into menu + ActionMenuList

const BlockOptionsComponent = () => {
  const { isOpen, getRootProps, close } = useBlockOptions();
  const { floatingBlockId, toggle: toggleFloating } = useFloatingBlockActionsActions();
  const { open: openActionMenuList } = useActionMenuListActions();

  const onTurnInto = (e: React.MouseEvent) => {
    if (!floatingBlockId) return;

    toggleFloating('frozen', floatingBlockId);
    openActionMenuList({
      reference: e.currentTarget as HTMLElement,
      view: 'small',
      placement: 'right',
      blockId: floatingBlockId,
    });
  };

  if (!isOpen) return null;

  return (
    <BlockOptions.Root {...getRootProps()} onClose={close}>
      <BlockOptions.Group>
        <BlockOptions.Button onClick={onTurnInto} icon={<SparklesIcon size={16} />}>
          Turn into
        </BlockOptions.Button>
      </BlockOptions.Group>
      <BlockOptions.Separator />
      <BlockOptions.Group>
        <BlockOptions.Button variant="destructive" icon={<Trash2Icon size={16} />}>
          Delete
        </BlockOptions.Button>
      </BlockOptions.Group>
    </BlockOptions.Root>
  );
};

2. Custom Actions Based on Block Type

const { blockId } = useBlockOptionsActions();
const block = blockId ? editor.blocks.getBlock(blockId) : null;
const isImage = block?.type === 'Image';

return (
  <BlockOptions.Root {...getRootProps()}>
    <BlockOptions.Group>
      <BlockOptions.Button onClick={handleDuplicate}>Duplicate</BlockOptions.Button>
      {isImage && (
        <BlockOptions.Button onClick={handleReplaceImage}>Replace image</BlockOptions.Button>
      )}
    </BlockOptions.Group>
    <BlockOptions.Separator />
    <BlockOptions.Group>
      <BlockOptions.Button variant="destructive" onClick={handleDelete}>
        Delete
      </BlockOptions.Button>
    </BlockOptions.Group>
  </BlockOptions.Root>
);

3. Integrating with FloatingBlockActions

const { open: openBlockOptions } = useBlockOptionsActions();

const onDragClick = (e: React.MouseEvent) => {
  if (!floatingBlockId) return;
  openBlockOptions({
    reference: e.currentTarget as HTMLElement,
    blockId: floatingBlockId,
  });
};

<FloatingBlockActions.Button onClick={onDragClick}>
  <GripVerticalIcon size={16} />
</FloatingBlockActions.Button>;

4. With Icons + Shortcuts

<BlockOptions.Button
  onClick={handleDuplicate}
  icon={<CopyIcon size={14} />}
  title="Duplicate block (⌘D)">
  Duplicate
  <kbd className="shortcut">⌘D</kbd>
</BlockOptions.Button>

Styling

CSS Variables

:root {
  --yoopta-ui-block-options-bg: var(--yoopta-ui-background);
  --yoopta-ui-block-options-border: var(--yoopta-ui-border);
  --yoopta-ui-block-options-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 /
          0.1);
  --yoopta-ui-block-options-radius: 0.5rem;
  --yoopta-ui-block-options-padding: 4px;
  --yoopta-ui-block-options-button-radius: 0.375rem;
  --yoopta-ui-block-options-button-hover: var(--yoopta-ui-accent);
  --yoopta-ui-block-options-button-destructive-color: hsl(var(--yoopta-ui-destructive));
}

Custom CSS Classes

<BlockOptions.Root className="glass-panel">
  <BlockOptions.Button className="ghost">Duplicate</BlockOptions.Button>
</BlockOptions.Root>
.glass-panel {
  background: rgba(15, 23, 42, 0.75);
  border: 1px solid rgba(255, 255, 255, 0.1);
  backdrop-filter: blur(16px);
}

.ghost {
  color: white;
  transition: transform 0.2s ease;
}

.ghost:hover {
  transform: translateX(4px);
}

Inline Styles / Tailwind

<BlockOptions.Root className="bg-slate-900/90 border border-white/10 shadow-2xl">
  <BlockOptions.Button className="text-white hover:bg-white/10">Duplicate</BlockOptions.Button>
</BlockOptions.Root>

Accessibility

BlockOptions handles focus trapping via the overlay, but keep these in mind:
  • Use title or aria-label on buttons for screen readers
  • Ensure destructive actions are clearly indicated (variant + icon + tooltip)
  • Keyboard users can reach buttons via Tab because buttons use type="button"
<BlockOptions.Button
  onClick={handleDelete}
  variant="destructive"
  aria-label="Delete current block"
  title="Delete block (Shift+⌘+Delete)">
  Delete
</BlockOptions.Button>

Best Practices

const { open } = useBlockOptionsActions();

open({
  reference: e.currentTarget,
  blockId: floatingBlockId, // required for helper actions
});
const { toggle: toggleFloating } = useFloatingBlockActionsActions();

const onOpenBlockOptions = () => {
  toggleFloating('frozen', floatingBlockId);
  openBlockOptions({ reference: element, blockId: floatingBlockId });
};

const onClose = () => {
  toggleFloating('hovering', floatingBlockId);
  closeBlockOptions();
};
<BlockOptions.Button variant="destructive" onClick={handleDelete}>
  Delete
</BlockOptions.Button>
const handleDuplicate = () => {
  duplicateBlock(floatingBlockId!);
  close(); // keep UI in sync
};