---
title: Block menu
description: The block menu plugin opens Notion's block menu when you click a block handle's grip, with search, Turn into, Duplicate, Move and Delete, and binds Mod+D to duplicate.
icon: menu
---

`blockMenuPlugin` opens a menu when you click a block handle's ⋮⋮ grip: a search field, Turn into, Duplicate, Move up, Move down and Delete, as in Notion. It also binds <kbd>Mod</kbd> + <kbd>D</kbd> to duplicate a block.

```svelte
<script lang="ts">
	import { Edytor, blockMenuPlugin } from 'edytor';
</script>

<Edytor plugins={[blockMenuPlugin]} />
```

The menu opens from [block handles](/docs/plugins/block-handles), which `<Edytor>` adds by default. Pass no `onActivate` to them: a handle with an `onActivate` callback calls it instead, and the menu never opens.

## Options

`blockMenuPlugin` has no block link and the built-in markup. `createBlockMenuPlugin(options)` builds one with options:

```ts
import { createBlockMenuPlugin } from 'edytor';

const blockMenu = createBlockMenuPlugin({
	linkTo: (block) => `${location.origin}${location.pathname}#block-${block.id}`
});
```

| Prop | Type | Default | Description |
| - | - | - | - |
| `linkTo?` | `(block: Block) => string` | - | A URL for the block. Adds a 'Copy link to block' row that writes it to the clipboard. Without it, the row is hidden. |
| `menu?` | `Snippet<[BlockMenuController]>` | - | Replaces the menu's markup. See Custom markup below. |

The options type is `BlockMenuOptions`.

## What it shows

The menu opens beside the grip, top-aligned 8px to its right (to its left when the right has no room, flipped up when the space below is short), follows scrolling and resizing, and closes when the grip leaves the view. From top to bottom:

1. A "Search actions…" field, focused.
2. A heading naming the block's kind, such as "Heading 2".
3. The actions:

| Row                | Hint                                              | Does                                                                   |
| ------------------ | ------------------------------------------------- | ---------------------------------------------------------------------- |
| Turn into ›        |                                                   | Opens a flyout of the kinds that keep content, with ✓ on the current one |
| Copy link to block |                                                   | Writes `linkTo(block)` to the clipboard. Only with `linkTo`            |
| Duplicate          | <kbd>Mod</kbd> + <kbd>D</kbd>                     | Inserts a copy after the block, with fresh ids for it, its children and its atoms |
| Move up            | <kbd>Mod</kbd> + <kbd>Shift</kbd> + <kbd>↑</kbd>  | Moves the block one step up                                            |
| Move down          | <kbd>Mod</kbd> + <kbd>Shift</kbd> + <kbd>↓</kbd>  | Moves the block one step down                                          |
| Delete             | <kbd>Del</kbd>                                    | Removes the block; its children take its place (a list or a code block goes with everything in it) |

A row that cannot apply is hidden: Turn into when nothing it applies to is convertible (a void block, an island or a block inside one; a list is never converted, its items are), Move up or Move down when `edytor.canMoveBlocks` refuses the step. The hints show the matching shortcuts; <kbd>Mod</kbd> + <kbd>Shift</kbd> + <kbd>↑</kbd>/<kbd>↓</kbd> need the [arrow move](/docs/plugins/arrow-move) plugin.

Typing in the field filters the actions by label, and lists the kinds whose label or keywords match under a "Turn into" heading: type `head` and pick "Heading 2" without opening the flyout. The query matches as in the [slash menu](/docs/plugins/slash-menu#what-it-lists): each word starts a word of the label or of a keyword, hyphens ignored, so `todo` and `to do` find "To-do list", `move` finds Move up and Move down, and a letter inside a word (`o` in "Move") matches nothing. With nothing left, the menu shows "No results".

## Keys

While the menu is open, its search field takes the keys:

| Keys                        | Action                                                          |
| --------------------------- | --------------------------------------------------------------- |
| <kbd>↑</kbd>, <kbd>↓</kbd>  | Move the highlight (wraps around); in the flyout, walk its kinds |
| <kbd>Home</kbd>, <kbd>End</kbd> | Highlight the first or last row                             |
| <kbd>→</kbd>                | Open the Turn into flyout from its row                          |
| <kbd>←</kbd>                | Close the flyout                                                |
| <kbd>Enter</kbd>            | Run the highlighted row, open the flyout from Turn into, or convert to the flyout's highlighted kind |
| <kbd>Delete</kbd>           | Delete the block, or the selected blocks (with an empty search) |
| <kbd>Escape</kbd>           | Close the flyout, then the menu                                 |

Hovering a row highlights it, in the menu and in the flyout.

## Behavior

- The handle click selects the block, and it stays selected while the menu is open.
- A list or a code block is one block to the menu, as clicked: Copy link to block shows, Move and Duplicate take it whole, and <kbd>Escape</kbd> returns the caret to its first line. Delete removes it with everything in it, and Turn into converts a list's items, which stay selected (a code block's lines never convert).
- A click on the grip of a block inside a block selection keeps the selection, and the actions apply to every selected block, as in Notion: Turn into converts them all (a list's items, never the list itself; an item turned into another kind leaves the list), Duplicate copies each one after itself, Move up and Move down move the group, and Delete removes them, each as one undo step. After Turn into, Move and <kbd>Escape</kbd> the blocks stay selected; after Delete the caret goes where the keyboard's block delete puts it (below). Copy link to block is hidden for several blocks.
- Every action closes the menu and returns a text caret: at the start of the converted or moved block, of the copy after Duplicate, and after Delete where <kbd>Backspace</kbd> on the same block selection puts it: at the start of its first child when it had children (they take its place), else at the end of the nearest text before it, else at the start of the next text; void blocks such as dividers are skipped, and so is a closed toggle's hidden body (the caret ends the toggle's header instead). When a plugin refuses the conversion or the deletion, the block stays and the caret returns to its start. <kbd>Escape</kbd> also returns the caret to the block. A block that holds no text, such as a divider, stays selected instead after Move, Escape or a refusal, and Duplicate selects its copy; an image's caret goes to the start of its caption.
- A pointer press outside the menu and the handles closes it without moving the caret, and so do a click on a `+` (which opens the slash menu instead) and the editor turning readonly.
- Each action is one undo step, separate from the typing before it however soon it follows, through the same commands as the rest of the editor (`convertToKind`, `duplicateBlock`, `edytor.moveBlocks`, `deleteBlocks`), so plugins can refuse them. Delete is one command, as the keyboard's block delete: `onDeleteSelectedBlocks` runs first, and its `prevent()` or a veto keeps every block. Over several blocks, Turn into and Duplicate skip a block a plugin vetoes and apply to the others ([Refusing an edit](/docs/plugins/operations#refusing-an-edit)).

<kbd>Mod</kbd> + <kbd>D</kbd> duplicates every selected block, in document order and as one undo step, each copy after its block, and selects the copies. Without a block selection it duplicates the block holding the caret and puts the caret in the copy. It is claimed only for a block that can move, and never in a readonly editor.

## How it opens

When a handle is clicked and no `onActivate` is set, block handles dispatch a DOM event on the editor's root:

```ts
import { BLOCK_ACTIVATE_EVENT, type BlockActivation } from 'edytor';
// 'edytor-block-activate', detail: { block, anchor }
```

`block` is the clicked block and `anchor` the grip element. The block menu listens for it. A plugin of your own can listen for the same event in `onEdytorAttached` to open a different menu, or to add behavior next to this one.

## Placement and styling

The menu mounts in the editor's overlay, in a `position: fixed` host with the attribute `data-edytor-block-menu-host` and `z-index: 70`. Its markup and styles are built in: a 265px panel with line icons for the built-in kinds and actions, the kind flyout beside it, and a 140ms fade and scale-in. The panel has `data-edytor-block-menu` and `data-testid="block-menu"`, and each action row `data-testid="block-menu-<id>"`, with ids `turn`, `link`, `duplicate`, `up`, `down` and `delete`.

## Custom markup

A `menu` snippet replaces the panel. It renders while `controller.isOpen`, in the same host, placed beside the grip by the element marked `data-edytor-block-menu` (else your snippet's first element). The plugin keeps opening it on a grip click, closing it on a press outside, placing it, and <kbd>Mod</kbd> + <kbd>D</kbd>. The keys in the table above belong to the built-in search field: a custom menu handles its own keys.

The snippet receives the `BlockMenuController`:

| Prop | Type | Default | Description |
| - | - | - | - |
| `block?` | `Block \| null` | - | The block whose grip opened the menu. |
| `blocks?` | `Block[]` | - | The blocks the actions apply to, in document order, as clicked: the block selection when it holds block, else block alone. A grip-selected list is one block. |
| `members?` | `Block[]` | - | What Delete and Turn into act on: blocks, a list or a code block with its whole subtree (selection.selectedMembers). |
| `actions?` | `BlockMenuAction[]` | - | The rows that apply, filtered by query: { id, label, icon, hint?, danger?, submenu?, run? }. Turn into has submenu: true and no run. |
| `kinds?` | `KindRow[]` | - | The kinds the block may turn into while keeping its content. |
| `currentKind?` | `KindRow \| undefined` | - | The row naming the block: of its kind's rows, the one whose preset data shares the most values with the block's (the first on a tie), so a checked to-do is still To-do list. |
| `turnInto(kind)?` | `(kind: KindRow) => void` | - | Convert the members (a list's items). |
| `duplicate(block)?` | `(block: Block) => void` | - | Insert a copy after the block (block.duplicateBlock(): fresh ids, one undo step). |
| `duplicateAll(blocks)?` | `(blocks: Block[]) => void` | - | Copy each block after itself as one undo step, and select the copies. The Duplicate action calls it for several blocks. |
| `move(direction)?` | `('up' \| 'down') => void` | - | Move the blocks one step. |
| `remove()?` | `() => void` | - | Delete the members; unselected children take their parent's place. |
| `copyLink()?` | `() => Promise<void>` | - | Write linkTo(block) to the clipboard. |
| `close()?` | `(restoreCaret?: boolean) => void` | - | Close the menu; the caret returns to the block (several blocks stay selected) unless restoreCaret is false. |
| `query?` | `string` | - | The search text. Writable: actions filter by it. |
| `selectedIndex?` | `number` | - | The keyboard's row. Writable. |
| `flyout?` | `boolean` | - | Whether the Turn into flyout is open. Writable. |
| `flyoutIndex?` | `number` | - | The keyboard's kind in the flyout. Writable. |
| `openFlyout()?` | `() => void` | - | Open the flyout on its first kind; an open flyout stays as it is. |

Every action closes the menu and returns the caret, as in the built-in menu: it replaces the block selection the grip click left, and the projector draws it after the flush.

```svelte title="Editor.svelte"
<script lang="ts">
	import { Edytor, createBlockMenuPlugin, type BlockMenuController } from 'edytor';

	const plugins = [createBlockMenuPlugin({ menu })];
</script>

{#snippet menu(controller: BlockMenuController)}
	<div class="menu" data-edytor-block-menu role="menu">
		{#each controller.actions.filter((action) => action.run) as action (action.id)}
			<button role="menuitem" onclick={() => action.run?.()}>{action.label}</button>
		{/each}
		{#each controller.kinds as kind (kind.id)}
			<button role="menuitem" onclick={() => controller.turnInto(kind)}>{kind.label}</button>
		{/each}
	</div>
{/snippet}

<Edytor {plugins} />
```

To open a different menu without this plugin, listen for `edytor-block-activate`, or pass `onActivate` to the block handles (see [Building a block menu](/docs/plugins/block-handles#building-a-block-menu)). [Menus and handles](/docs/customization/menus) shows the other snippets.
