---
title: Slash menu
description: The slash menu plugin opens a filtered command menu when you type a slash, listing every block kind preset and every plugin command.
icon: square-slash
---

`slashMenuPlugin` opens a command menu at the caret when you type `/`. Typing after the slash filters it; <kbd>Enter</kbd> runs the highlighted command and removes the `/query` text.

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

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

`slashMenuPlugin` is the menu with its built-in markup. `createSlashMenuPlugin(options)` builds one that renders your markup (see [Custom markup](#custom-markup)).

## What it lists

The menu lists `edytor.commands`:

- one command per block kind preset, such as `block.heading2` "Heading 2" (see [Blocks](/docs/customization/blocks#presets));
- every command a plugin declares in `commands` (see [Writing plugins](/docs/plugins/writing-plugins#commands)).

A command whose `isEnabled(edytor)` answers `false` is hidden (except in the menu the block handle's `+` opens, below). Block kind commands are enabled when the caret's block is convertible, which excludes void blocks, island blocks and blocks inside them. A kind picked in a list's item takes the item out of the list ([Containers](/docs/concepts/blocks#containers)).

Commands are grouped into sections by their `group`, as in Notion: `Basic blocks` first, then the other groups in the order they first appear. Kind commands take their preset's `group` (default `Basic blocks`; the image and code blocks are under `Media`), and a command without a `group` is listed without a heading. Each row shows the command's `hint` on the right; kind commands use their first markdown prefix, such as `##` for Heading 2.

The query matches, case-insensitively, the start of a word in a command's label or keywords (hyphens are ignored, so `/todo` finds "To-do list"). `/h2`, `/head` and `/subtitle` all find "Heading 2"; `/eading` finds nothing. A query of several words keeps the menu open while each word, in order, starts a word of the same label or keyword, or continues the word the one before it started: `/bullet list` finds "Bulleted list", `/to do` finds "To-do list", and `/list bullet` finds nothing. The command id (`block.heading2`) is not searched. The [block menu](/docs/plugins/block-menu)'s search matches with the same rule.

## Keys

While the menu is open:

| Keys                                   | Action                                     |
| -------------------------------------- | ------------------------------------------ |
| <kbd>↑</kbd>, <kbd>↓</kbd>             | Move the highlight (wraps around), across sections; typing moves it back to the first match |
| <kbd>Enter</kbd>                       | Run the highlighted command                |
| <kbd>Escape</kbd>                      | Close the menu, keep the typed text        |
| Hover                                  | Highlight that command                     |
| Click                                  | Run that command                           |

<kbd>Enter</kbd> is claimed only when at least one command matches. With no match it inserts a new block as usual.

## Behavior

- The menu opens when `/` is typed at a collapsed caret in a convertible block, at the start of a text or after whitespace, as in Notion. A `/` inside a word stays text: `see 1/2`, `a/b` and `and/or` followed by <kbd>Enter</kbd> keep their text and start a new block. An IME that commits `/` together with more text (`/h`) opens it with that query.
- It closes when the caret leaves the query, the selection expands, the `/` is deleted, or the query matches no command (a `/` in a URL or a path stays text, and <kbd>↑</kbd>/<kbd>↓</kbd> move the caret again). A space right after the `/` closes it too, and so does a query of only hyphens: `yes / no`, `1 / 2` and `x /-` followed by <kbd>Enter</kbd> keep their text and start a new block. "No results" shows only for a bare `/` when no command is enabled. It does not open in a readonly editor, and it closes when the editor turns readonly.
- The query range is anchored in the document, so a collaborator's edits elsewhere in the text do not break it.
- Running a command removes the `/query` text in the same step as the command, separate from the typing before it, so one undo restores both: `hi /h2` + <kbd>Enter</kbd> undoes to a paragraph `hi /h2`. If the command is refused, the trigger text stays.
- After a conversion the caret goes back to where the `/` was. A command that moves the caret elsewhere keeps its own caret: the code block conversion puts it in the first line, and `/divider` in a fresh block of the default kind where the divider lands, after it (a divider renders no content, so it cannot hold the caret).
- A kind that replaces the block's content (`/divider`, `/code`) converts the block in place only when it holds nothing but the query. In a block with other text or with children, the new block goes after it and the block stays intact: `keep me /divider` + <kbd>Enter</kbd> leaves `keep me ` above a divider. After a list's item, the new block goes outside the list, which splits after the item; in an empty item, the item leaves the list as the new kind. The query's removal and the insertion are one step.

## From the block handle

The block handle's `+` opens the menu too, without typing anything ([Block handles](/docs/plugins/block-handles)). The menu then shows a search field, focused, instead of reading a `/query` from the text, and lists every command. <kbd>↑</kbd>/<kbd>↓</kbd>, <kbd>Enter</kbd> and <kbd>Escape</kbd> work in the field as above (not while an IME composes: the <kbd>Enter</kbd> or <kbd>Escape</kbd> that ends a composition is the IME's); a query that matches nothing shows "No results" and keeps the menu open. Picking a command adds an empty block of the default kind below the block (above with <kbd>Alt</kbd>) and runs the command in it, as one undo step; when the block beside the `+` is already an empty block of that kind, the command runs in it instead. The block does not exist yet when the menu opens, so its `isEnabled` cannot be asked then: every command is listed, and one that turns out not to apply adds nothing. When its `isEnabled` answers `false` in the new block, or the document or an extension refuses it before it changes anything, the block is taken back: the document, the undo history and the selection are as they were, and `edytor.dispatcher.last` reads `refused`. The kind lands where it fits, as the slash menu places it on an empty line: beside a list's item, a heading splits the list after the item, and a list's own kind adds an item to it; a divider leaves the caret on an empty line after it. <kbd>Escape</kbd>, the footer, a press outside, focus moving to an element outside the menu or the editor turning readonly close the menu with nothing added and no undo step, and give the selection back as it was; so does activating a block's grip, and so does a delete of the block beside the `+` (a collaborator's, say). <kbd>Escape</kbd> and the footer also give the editor its focus back. The menu opens below the `+`. `controller.addition` is set while it is open this way.

## Placement and styling

The menu mounts in the editor's overlay, in a `position: fixed` host with the attribute `data-edytor-slash-menu-host` and `z-index: 50`. It opens below the caret, or above it when there is no room, and stays inside the viewport while you scroll.

The menu's markup and styles are built in, after Notion's: sections with a heading, 28px rows with a line icon for the built-in kinds (a command's text `icon` otherwise), the `hint` on the right, "No results" for a bare `/` when no command is enabled, and a "Close menu · esc" footer that closes it on click. It fades and scales in over 140ms, and the highlighted row scrolls into view. The typed query is not shown: it stays in a visually hidden element with `data-testid="slash-menu-query"` for screen readers and tests. Each item carries `data-command-id`, `data-selected` and `data-hint`.

## Custom markup

`createSlashMenuPlugin` takes snippets that replace the menu's markup. The plugin keeps everything else: opening on `/`, the query, the filtering, <kbd>↑</kbd>/<kbd>↓</kbd>, <kbd>Enter</kbd> and <kbd>Escape</kbd>, and the placement.

| Prop | Type | Default | Description |
| - | - | - | - |
| `item?` | `Snippet<[SlashMenuItem]>` | - | Replaces each row of the built-in menu. The sections, headings, empty state and footer stay. |
| `menu?` | `Snippet<[SlashMenuController]>` | - | Replaces the whole menu. Rendered while controller.isOpen. Wins over item. |

An `item` snippet receives a `SlashMenuItem`:

| Prop | Type | Default | Description |
| - | - | - | - |
| `command?` | `EditorCommand` | - | The command: id, label, icon, hint, group, keywords. |
| `selected?` | `boolean` | - | It is the keyboard's row. |
| `icon?` | `string \| undefined` | - | The built-in line icon as a CSS mask-image value, for the commands that have one. |
| `run?` | `() => void` | - | Run the command; the /query text is removed in the same step. |
| `select?` | `() => void` | - | Make it the keyboard's row, for hover. |

A `menu` snippet receives the `SlashMenuController`. Read `controller.commands` (the filtered, enabled commands in menu order), `controller.selectedIndex` and `controller.query`; call `controller.run(command)` to run one and `controller.close()` to close the menu and keep the typed text.

When the block handle's `+` opens the menu (`controller.addition` is set), the plugin renders your `menu` inside a focused wrapper that takes the keyboard from the editor: typed characters and <kbd>Backspace</kbd> edit `controller.query`, and <kbd>↑</kbd>/<kbd>↓</kbd>, <kbd>Enter</kbd> and <kbd>Escape</kbd> work as above, so nothing typed reaches the document. A snippet with a search field of its own calls `controller.search(query)` from it; the wrapper's keys still move, pick and close. Call `controller.dismiss()` rather than `close()` to close it: it gives the editor back its focus and selection.

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

	const plugins = [createSlashMenuPlugin({ item })];
</script>

{#snippet item({ command, selected, run, select }: SlashMenuItem)}
	<button
		class="row"
		class:selected
		onmousedown={(event) => event.preventDefault()}
		onmousemove={select}
		onclick={run}
	>
		{command.icon} {command.label}
	</button>
{/snippet}

<Edytor {plugins} />
```

Keep `preventDefault` on `mousedown`, so a click does not move the caret out of the query. Your markup renders in the same fixed host, so the built-in styles no longer apply. See [Menus and handles](/docs/customization/menus) for the other menus.
