Skip to main content

Overview

BlockOptions is compound component for block context menus. It supports controlled/uncontrolled modes, Floating UI positioning, and integrates seamlessly with other UI components.
Block options menu example

Features

  • API — controlled/uncontrolled with open/onOpenChange
  • Compound components — Root, Content, Trigger, Item, Group, Separator
  • Floating positioning — automatic positioning via Floating UI
  • Helper hookuseBlockActions for common operations

Installation

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

Basic Usage

import { BlockOptions, useBlockActions } from '@yoopta/ui/block-options';

function MyBlockOptions({ open, onOpenChange, blockId, anchor }) {
  const { duplicateBlock, copyBlockLink, deleteBlock } = useBlockActions();

  return (
    <BlockOptions open={open} onOpenChange={onOpenChange} anchor={anchor}>
      <BlockOptions.Content side="right" align="start">
        <BlockOptions.Group>
          <BlockOptions.Item onSelect={() => duplicateBlock(blockId)}>Duplicate</BlockOptions.Item>
          <BlockOptions.Item onSelect={() => copyBlockLink(blockId)}>Copy link</BlockOptions.Item>
        </BlockOptions.Group>
        <BlockOptions.Separator />
        <BlockOptions.Group>
          <BlockOptions.Item variant="destructive" onSelect={() => deleteBlock(blockId)}>
            Delete
          </BlockOptions.Item>
        </BlockOptions.Group>
      </BlockOptions.Content>
    </BlockOptions>
  );
}

API Reference

BlockOptions (Root)

Root component that manages open state and context.
<BlockOptions open={isOpen} onOpenChange={setIsOpen} anchor={anchorElement} defaultOpen={false}>
  {children}
</BlockOptions>
Props:
PropTypeDescription
openbooleanControlled open state
onOpenChange(open: boolean) => voidCalled when open state changes
defaultOpenbooleanDefault open state (uncontrolled)
anchorHTMLElement | nullAnchor element for positioning
childrenReactNodeContent and trigger components

BlockOptions.Trigger

Optional trigger button (for uncontrolled usage).
<BlockOptions>
  <BlockOptions.Trigger asChild>
    <button>Open Menu</button>
  </BlockOptions.Trigger>
  <BlockOptions.Content>...</BlockOptions.Content>
</BlockOptions>
Props:
PropTypeDescription
asChildbooleanMerge props onto child element
classNamestringCustom CSS classes

BlockOptions.Content

Floating content panel.
<BlockOptions.Content side="right" align="start" sideOffset={5}>
  {/* Items here */}
</BlockOptions.Content>
Props:
PropTypeDescription
side'top' | 'right' | 'bottom' | 'left'Placement side
align'start' | 'center' | 'end'Alignment
sideOffsetnumberOffset from anchor (default: 5)
classNamestringCustom CSS classes

BlockOptions.Item

Menu item button.
<BlockOptions.Item onSelect={handleAction} variant="default" icon={<CopyIcon />} keepOpen={false}>
  Duplicate
</BlockOptions.Item>
Props:
PropTypeDescription
onSelect(event) => voidCalled when item is selected
variant'default' | 'destructive'Visual variant
iconReactNodeIcon to display
keepOpenbooleanKeep menu open after selection
disabledbooleanDisable the item

BlockOptions.Group

Groups items together.
<BlockOptions.Group>
  <BlockOptions.Item>Item 1</BlockOptions.Item>
  <BlockOptions.Item>Item 2</BlockOptions.Item>
</BlockOptions.Group>

BlockOptions.Separator

Visual separator between groups.
<BlockOptions.Separator />

useBlockActions()

Helper hook with common block operations.
const { duplicateBlock, copyBlockLink, deleteBlock } = useBlockActions();

// Usage
duplicateBlock(blockId);
copyBlockLink(blockId);
deleteBlock(blockId);

Examples

With ActionMenuList (Turn Into)

function MyBlockOptions({ open, onOpenChange, blockId, anchor }) {
  const { duplicateBlock, deleteBlock } = useBlockActions();
  const turnIntoRef = useRef<HTMLButtonElement>(null);
  const [actionMenuOpen, setActionMenuOpen] = useState(false);

  const onActionMenuClose = (menuOpen: boolean) => {
    setActionMenuOpen(menuOpen);
    if (!menuOpen) onOpenChange?.(false); // Close both
  };

  return (
    <>
      <BlockOptions open={open} onOpenChange={onOpenChange} anchor={anchor}>
        <BlockOptions.Content side="right">
          <BlockOptions.Group>
            <BlockOptions.Item ref={turnIntoRef} onSelect={() => setActionMenuOpen(true)} keepOpen>
              Turn into
            </BlockOptions.Item>
          </BlockOptions.Group>
          <BlockOptions.Separator />
          <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>

      <ActionMenuList
        open={actionMenuOpen}
        onOpenChange={onActionMenuClose}
        anchor={turnIntoRef.current}
        blockId={blockId}
        view="small"
        placement="right-start">
        <ActionMenuList.Content />
      </ActionMenuList>
    </>
  );
}

Uncontrolled with Trigger

<BlockOptions>
  <BlockOptions.Trigger>
    <button>Open Options</button>
  </BlockOptions.Trigger>
  <BlockOptions.Content>
    <BlockOptions.Group>
      <BlockOptions.Item onSelect={handleDuplicate}>Duplicate</BlockOptions.Item>
      <BlockOptions.Item variant="destructive" onSelect={handleDelete}>
        Delete
      </BlockOptions.Item>
    </BlockOptions.Group>
  </BlockOptions.Content>
</BlockOptions>

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);
  --yoopta-ui-block-options-radius: 0.5rem;
  --yoopta-ui-block-options-button-hover: var(--yoopta-ui-accent);
  --yoopta-ui-block-options-button-destructive-color: hsl(var(--yoopta-ui-destructive));
}

Best Practices

<BlockOptions.Item
  onSelect={() => setActionMenuOpen(true)}
  keepOpen // Prevents BlockOptions from closing
>
  Turn into
</BlockOptions.Item>
const onActionMenuClose = (menuOpen: boolean) => {
  setActionMenuOpen(menuOpen);
  if (!menuOpen) {
    onOpenChange?.(false); // Also close BlockOptions
  }
};