Skip to main content
The BlockOptions component provides a dropdown menu for block-level actions like duplicating, deleting, or converting blocks. It’s typically triggered from FloatingBlockActions or custom UI elements.

Installation

npm install @yoopta/ui

Basic Usage

import { BlockOptions, useBlockActions } from '@yoopta/ui';
import { useState } from 'react';

function MyBlockOptions({ blockId, anchor }) {
  const [open, setOpen] = useState(false);
  const { duplicateBlock, deleteBlock } = useBlockActions();

  return (
    <BlockOptions open={open} onOpenChange={setOpen} anchor={anchor}>
      <BlockOptions.Content>
        <BlockOptions.Group>
          <BlockOptions.Item onSelect={() => duplicateBlock(blockId)}>
            Duplicate
          </BlockOptions.Item>
          <BlockOptions.Item 
            variant="destructive" 
            onSelect={() => deleteBlock(blockId)}
          >
            Delete
          </BlockOptions.Item>
        </BlockOptions.Group>
      </BlockOptions.Content>
    </BlockOptions>
  );
}

Component API

BlockOptions (Root)

The root component that manages menu state.
children
ReactNode
required
Menu content (typically BlockOptions.Content)
open
boolean
Controlled open state
onOpenChange
(open: boolean) => void
Callback when open state changes
defaultOpen
boolean
default:"false"
Default open state for uncontrolled usage
anchor
HTMLElement | null
External anchor element for positioning (use when no Trigger is present)

BlockOptions.Trigger

Trigger button for opening the menu.
children
ReactNode
required
Button content
asChild
boolean
default:"false"
Merge props onto child element instead of wrapping
className
string
Additional CSS class name

BlockOptions.Content

The floating menu content.
children
ReactNode
required
Menu items and groups
side
'top' | 'right' | 'bottom' | 'left'
default:"'right'"
Placement relative to trigger
align
'start' | 'center' | 'end'
default:"'start'"
Alignment relative to trigger
sideOffset
number
default:"5"
Offset from trigger in pixels
className
string
Additional CSS class name

BlockOptions.Group

Groups related menu items.
children
ReactNode
required
Menu items
className
string
Additional CSS class name

BlockOptions.Item

children
ReactNode
required
Item label
onSelect
(event: MouseEvent) => void
Called when item is selected
disabled
boolean
Whether the item is disabled
icon
ReactNode
Optional icon element
variant
'default' | 'destructive'
default:"'default'"
Visual variant
keepOpen
boolean
default:"false"
Keep menu open after selection
className
string
Additional CSS class name

BlockOptions.Separator

Visual separator between groups.
className
string
Additional CSS class name

BlockOptions.Label

Group label text.
children
ReactNode
required
Label text
className
string
Additional CSS class name

useBlockActions Hook

Helper hook providing common block actions:
const {
  duplicateBlock,  // (blockId: string) => void
  copyBlockLink,   // (blockId: string) => void
  deleteBlock,     // (blockId: string) => void
} = useBlockActions();

Examples

Complete Menu

import { BlockOptions, useBlockActions } from '@yoopta/ui';
import { useState, useRef } from 'react';
import { Copy, Link, Trash2 } from 'lucide-react';

function CompleteBlockOptions({ blockId, anchor }) {
  const [open, setOpen] = useState(false);
  const { duplicateBlock, copyBlockLink, deleteBlock } = useBlockActions();

  return (
    <BlockOptions open={open} onOpenChange={setOpen} anchor={anchor}>
      <BlockOptions.Content side="right" align="end">
        <BlockOptions.Group>
          <BlockOptions.Item
            icon={<Copy size={16} />}
            onSelect={() => duplicateBlock(blockId)}
          >
            Duplicate
          </BlockOptions.Item>
          <BlockOptions.Item
            icon={<Link size={16} />}
            onSelect={() => copyBlockLink(blockId)}
          >
            Copy link to block
          </BlockOptions.Item>
        </BlockOptions.Group>
        
        <BlockOptions.Separator />
        
        <BlockOptions.Group>
          <BlockOptions.Item
            icon={<Trash2 size={16} />}
            variant="destructive"
            onSelect={() => deleteBlock(blockId)}
          >
            Delete
          </BlockOptions.Item>
        </BlockOptions.Group>
      </BlockOptions.Content>
    </BlockOptions>
  );
}

With Turn Into Menu

import { BlockOptions } from '@yoopta/ui';
import { ActionMenuList } from '@yoopta/ui';
import { useState, useRef } from 'react';

function OptionsWithTurnInto({ blockId, anchor }) {
  const [optionsOpen, setOptionsOpen] = useState(false);
  const [actionMenuOpen, setActionMenuOpen] = useState(false);
  const turnIntoRef = useRef<HTMLButtonElement>(null);

  const onTurnInto = () => {
    setActionMenuOpen(true);
  };

  const onActionMenuClose = (open: boolean) => {
    setActionMenuOpen(open);
    if (!open) {
      setOptionsOpen(false);
    }
  };

  return (
    <>
      <BlockOptions open={optionsOpen} onOpenChange={setOptionsOpen} anchor={anchor}>
        <BlockOptions.Content>
          <BlockOptions.Group>
            <BlockOptions.Item
              ref={turnIntoRef}
              onSelect={onTurnInto}
              keepOpen
            >
              Turn into
            </BlockOptions.Item>
          </BlockOptions.Group>
          <BlockOptions.Separator />
          <BlockOptions.Group>
            <BlockOptions.Item>Duplicate</BlockOptions.Item>
            <BlockOptions.Item variant="destructive">Delete</BlockOptions.Item>
          </BlockOptions.Group>
        </BlockOptions.Content>
      </BlockOptions>
      
      <ActionMenuList
        placement="right-start"
        open={actionMenuOpen}
        onOpenChange={onActionMenuClose}
        anchor={turnIntoRef.current}
      />
    </>
  );
}

With Trigger Button

import { BlockOptions } from '@yoopta/ui';
import { MoreVertical } from 'lucide-react';

function OptionsWithTrigger() {
  return (
    <BlockOptions>
      <BlockOptions.Trigger>
        <button>
          <MoreVertical size={16} />
        </button>
      </BlockOptions.Trigger>
      
      <BlockOptions.Content>
        <BlockOptions.Item>Duplicate</BlockOptions.Item>
        <BlockOptions.Item>Delete</BlockOptions.Item>
      </BlockOptions.Content>
    </BlockOptions>
  );
}

As Child Pattern

import { BlockOptions } from '@yoopta/ui';
import { Button } from './ui/button';

function OptionsWithAsChild() {
  return (
    <BlockOptions>
      <BlockOptions.Trigger asChild>
        <Button variant="ghost" size="sm">
          Options
        </Button>
      </BlockOptions.Trigger>
      
      <BlockOptions.Content>
        {/* Items */}
      </BlockOptions.Content>
    </BlockOptions>
  );
}

Custom Actions

import { BlockOptions } from '@yoopta/ui';
import { useYooptaEditor, Blocks } from '@yoopta/editor';
import { Eye, EyeOff, Lock } from 'lucide-react';

function CustomBlockOptions({ blockId, anchor }) {
  const editor = useYooptaEditor();
  const [open, setOpen] = useState(false);

  const toggleVisibility = () => {
    // Custom logic to toggle block visibility
    const block = Blocks.getBlock(editor, { id: blockId });
    if (!block) return;
    
    Blocks.updateBlock(editor, {
      id: blockId,
      props: {
        ...block.props,
        hidden: !block.props?.hidden,
      },
    });
  };

  return (
    <BlockOptions open={open} onOpenChange={setOpen} anchor={anchor}>
      <BlockOptions.Content>
        <BlockOptions.Label>Visibility</BlockOptions.Label>
        <BlockOptions.Group>
          <BlockOptions.Item
            icon={<Eye size={16} />}
            onSelect={toggleVisibility}
          >
            Toggle visibility
          </BlockOptions.Item>
          <BlockOptions.Item icon={<Lock size={16} />}>
            Lock block
          </BlockOptions.Item>
        </BlockOptions.Group>
      </BlockOptions.Content>
    </BlockOptions>
  );
}

Conditional Items

import { BlockOptions } from '@yoopta/ui';
import { useYooptaEditor } from '@yoopta/editor';

function ConditionalOptions({ blockId, anchor }) {
  const editor = useYooptaEditor();
  const block = editor.children[blockId];
  const canMoveUp = block?.meta.order > 0;
  const canMoveDown = block?.meta.order < Object.keys(editor.children).length - 1;

  return (
    <BlockOptions anchor={anchor}>
      <BlockOptions.Content>
        <BlockOptions.Group>
          {canMoveUp && (
            <BlockOptions.Item>Move up</BlockOptions.Item>
          )}
          {canMoveDown && (
            <BlockOptions.Item>Move down</BlockOptions.Item>
          )}
        </BlockOptions.Group>
      </BlockOptions.Content>
    </BlockOptions>
  );
}

Behavior

The menu automatically closes when:
  • An item is selected (unless keepOpen={true})
  • User clicks outside
  • Escape key is pressed
  • Anchor element is removed

Positioning

Uses Floating UI for smart positioning:
  • Respects side and align props
  • Flips to opposite side if no space
  • Shifts to stay within viewport
  • Updates on scroll/resize

Focus Management

Provides keyboard navigation:
  • Arrow keys to navigate items
  • Enter to select
  • Escape to close
  • Tab to cycle through items

Styling

CSS Classes

.yoopta-ui-block-options {
  /* Menu container */
}

.yoopta-ui-block-options-trigger {
  /* Trigger button */
}

.yoopta-ui-block-options-group {
  /* Item group */
}

.yoopta-ui-block-options-button {
  /* Menu item */
}

.yoopta-ui-block-options-button-default {
  /* Default item variant */
}

.yoopta-ui-block-options-button-destructive {
  /* Destructive item variant */
}

.yoopta-ui-block-options-separator {
  /* Separator line */
}

.yoopta-ui-block-options-label {
  /* Group label */
}

Custom Styling

<BlockOptions>
  <BlockOptions.Content className="my-menu">
    <BlockOptions.Item className="my-item">
      Custom styled item
    </BlockOptions.Item>
  </BlockOptions.Content>
</BlockOptions>

TypeScript

import type {
  BlockOptionsRootProps,
  BlockOptionsTriggerProps,
  BlockOptionsContentProps,
  BlockOptionsGroupProps,
  BlockOptionsItemProps,
  BlockOptionsSeparatorProps,
  BlockOptionsLabelProps,
} from '@yoopta/ui';

See Also

Build docs developers (and LLMs) love