---
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. Block kind commands are enabled when the caret's block is convertible, which excludes void blocks, island blocks and blocks inside them.

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 parent's default kind after the divider (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. The query's removal and the insertion are one step.

## 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.

```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.
