Floating Menu
FloatingMenu shows a floating block-insert menu when the cursor sits at the start of an empty paragraph. Extensions contribute items via the addFloatingMenuItems() hook, items are grouped and ranked via FloatingMenuController, and keyboard navigation uses a roving-tabindex pattern with Alt-F10 and Mod-/ shortcuts.
FloatingMenu lives in @domternal/extension-block-menu. The headless FloatingMenuController and item types live in @domternal/core so framework wrappers can import them without pulling in the full block-menu package.
Not included in StarterKit. Install @domternal/extension-block-menu and add it explicitly.
Live Playground
Section titled “Live Playground”Click into the editor and press Enter to create an empty paragraph - the floating menu appears with all block-insert options. Or press Alt-F10 / Mod-/ to enter the menu via keyboard.
Installation
Section titled “Installation”pnpm add @domternal/extension-block-menunpm install @domternal/extension-block-menuyarn add @domternal/extension-block-menuimport { StarterKit } from '@domternal/core';import { FloatingMenu } from '@domternal/extension-block-menu';import { DomternalEditor, DomternalFloatingMenu } from '@domternal/vanilla';import '@domternal/theme';
const menuEl = document.getElementById('floating-menu')!;
const dm = new DomternalEditor(document.getElementById('editor')!, { extensions: [ StarterKit, FloatingMenu.configure({ element: menuEl }), ],});
new DomternalFloatingMenu(menuEl, { editor: dm.editor });@if (editor(); as ed) { <domternal-floating-menu [editor]="ed" />}The Angular component creates its own element and registers the plugin internally.
<Domternal extensions={[StarterKit, FloatingMenu]} content="<p></p>"> <Domternal.Toolbar /> <Domternal.Content /> <Domternal.FloatingMenu /></Domternal><Domternal :extensions="extensions" content="<p></p>"> <Domternal.Toolbar /> <Domternal.Content /> <Domternal.FloatingMenu /></Domternal>import { Editor, StarterKit, FloatingMenuController } from '@domternal/core';import { FloatingMenu, createFloatingMenuPlugin } from '@domternal/extension-block-menu';
const menuEl = document.createElement('div');menuEl.className = 'dm-floating-menu';document.body.append(menuEl);
const editor = new Editor({ element: document.getElementById('editor')!, extensions: [ StarterKit, FloatingMenu.configure({ element: menuEl }), ],});
const controller = new FloatingMenuController(editor, () => { // re-render the menu DOM from controller.groups, controller.focusedIndex});controller.subscribe();Options
Section titled “Options”FloatingMenu.configure({ element: menuEl, // required shouldShow: defaultShouldShow, // override visibility predicate offset: 0, // px offset from anchor items: undefined, // override items list keymap: { enterMenu: ['Alt-F10', 'Mod-/'] }, // shortcuts that focus first item requireExplicitTrigger: false, // Notion-style: only opens via showFloatingMenu()})| Option | Type | Default | Description |
|---|---|---|---|
element | HTMLElement | null | null | The HTML element that contains the menu. Required - if null, the plugin is not created. Framework wrappers create this element and pass it in. |
shouldShow | (props) => boolean | See below | Custom visibility predicate |
offset | number | 0 | Pixel offset from anchor |
items | FloatingMenuItemsOverride | undefined | Items override. Array replaces defaults; function receives collected defaults and returns a new list |
keymap | FloatingMenuKeymap | { enterMenu: ['Alt-F10', 'Mod-/'] } | Keyboard shortcuts for entering the menu via keyboard |
requireExplicitTrigger | boolean | false | When true, the menu only appears via showFloatingMenu(view). Notion-style behavior. |
Default shouldShow behavior
Section titled “Default shouldShow behavior”function defaultShouldShow({ editor, state }): boolean { if (!editor.isEditable) return false; const { $from, empty } = state.selection; if (!empty) return false; if ($from.parent.type.name !== 'paragraph') return false; if ($from.parent.content.size !== 0) return false; if ($from.parentOffset !== 0) return false; return true;}Shows when ALL conditions are met:
- The editor is editable
- The selection is empty (no range)
- The cursor’s parent is a
paragraph - The parent has zero content (empty)
- The cursor is at offset 0
requireExplicitTrigger - Notion mode
Section titled “requireExplicitTrigger - Notion mode”When true, the menu does NOT auto-show on empty paragraphs. The ONLY way it appears is via:
- A call to
showFloatingMenu(view)(typically fromBlockHandle’s+button) - The keyboard shortcuts in
keymap.enterMenu(e.g.Mod-/)
This matches Notion’s behavior: empty rows show a placeholder hint, the / slash command is the keyboard trigger, and the menu opens via the gutter + button only.
FloatingMenu.configure({ element: menuEl, requireExplicitTrigger: true,})Programmatic control
Section titled “Programmatic control”import { showFloatingMenu, hideFloatingMenu } from '@domternal/extension-block-menu';
showFloatingMenu(editor.view); // explicit triggerhideFloatingMenu(editor.view); // explicit dismissBoth dispatch a plugin meta (key: 'dm:floatingMenuTrigger') so they work regardless of which PluginKey instance the menu was registered under.
Items API
Section titled “Items API”Extensions contribute items via the addFloatingMenuItems() hook on the Extension class:
import { Extension } from '@domternal/core';
const MyExt = Extension.create({ name: 'myExt', addFloatingMenuItems() { return [ { name: 'myItem', label: 'My Item', description: 'Inserts a my-item block', icon: 'star', group: 'Basic', priority: 200, keywords: ['my', 'custom'], shortcut: 'M', command: 'insertMyBlock', commandArgs: [], isDisabled: (editor) => !editor.can().insertMyBlock(), hideWhenInside: ['codeBlock'], }, ]; },});FloatingMenuItem
Section titled “FloatingMenuItem”interface FloatingMenuItem { name: string; // unique id label: string; // display text description?: string; // longer hint icon?: string; // icon key from defaultIcons or custom IconSet group?: string; // group heading (preserves insertion order) priority?: number; // default 100, higher first within group keywords?: string[]; // additional terms for SlashCommand filtering shortcut?: string; // visible hint (no shortcut wiring) command: string | ((editor: Editor) => void); // string = editor.commands[name], function = direct call commandArgs?: unknown[]; // args for string commands isDisabled?: (editor: Editor) => boolean; // override default `editor.can()` check hideWhenInside?: string[]; // hide when cursor inside these node types}Default items contributed by core
Section titled “Default items contributed by core”The following extensions contribute items via addFloatingMenuItems:
| Extension | Item | Group |
|---|---|---|
Heading | Heading 1, 2, 3, 4, 5, 6 | Headings |
BulletList | Bullet list | Lists |
OrderedList | Numbered list | Lists |
TaskList | To-do list | Lists |
Blockquote | Quote | Text |
CodeBlock | Code block | Text |
HorizontalRule | Divider | Text |
extension-image’s Image | Image | Media |
extension-table’s Table | Table | Media |
extension-details’s Details | Toggle list (details) | Lists |
Override with FloatingMenu.configure({ items: ... }) - array replaces defaults, function transforms them.
Helper: groupFloatingMenuItems
Section titled “Helper: groupFloatingMenuItems”import { groupFloatingMenuItems } from '@domternal/core';
const groups = groupFloatingMenuItems(items);// [{ name: 'Headings', items: [...] }, { name: 'Lists', items: [...] }, ...]Groups items by .group preserving extension insertion order; sorts items within each group by priority descending.
FloatingMenuController
Section titled “FloatingMenuController”Headless state machine that framework wrappers (and the Vanilla wrapper) use to render the menu UI.
import { FloatingMenuController, FLOATING_MENU_NO_FOCUS } from '@domternal/core';
const controller = new FloatingMenuController(editor, () => { // onChange - re-render}, override);
controller.subscribe();// ...controller.destroy();Static helpers
Section titled “Static helpers”FloatingMenuController.resolveItems(editor, override?): FloatingMenuItem[];FloatingMenuController.executeItem(editor, item): void;resolveItems exposed as static so the plugin can resolve once at init without constructing a controller.
executeItem dispatches the item’s command - string -> editor.commands[name](...args); function -> direct call.
Instance API
Section titled “Instance API”class FloatingMenuController { groups: FloatingMenuGroup[]; // grouped items flatItems: FloatingMenuItem[]; // flattened disabledMap: ReadonlyMap<string, boolean>; // per-item disabled state focusedIndex: number; // -1 = no focus, else 0..flatItems.length-1 isEntered: boolean; // true once user enters via keyboard itemCount: number;
subscribe(): void; // wire transaction handler destroy(): void; // unsubscribe + cleanup
enterMenu(): number; // focus first item, returns its index leaveMenu(): void; // unfocus next(): number; // ArrowDown, wraps at end prev(): number; // ArrowUp, wraps at start first(): number; // Home last(): number; // End setFocusedIndex(index: number): void; // direct set
execute(item: FloatingMenuItem): void; // dispatch item's command isDisabled(item: FloatingMenuItem): boolean; focusedItem(): FloatingMenuItem | null;}FLOATING_MENU_NO_FOCUS constant equals -1.
Keyboard shortcuts
Section titled “Keyboard shortcuts”When keymap.enterMenu is non-empty, those shortcuts focus the first item if the menu is visible:
Alt-F10- WAI-ARIA recommendation for entering toolbars/menusMod-/- modern slash-command-friendly shortcut
Inside the menu (after entering):
- ArrowDown / ArrowUp - navigate items (wraps)
- Home / End - first / last
- Enter / Space - execute focused item
- Escape - leave menu, return focus to editor
Standalone plugin
Section titled “Standalone plugin”For framework wrappers or advanced use:
import { createFloatingMenuPlugin } from '@domternal/extension-block-menu';import { PluginKey } from '@domternal/pm/state';
const plugin = createFloatingMenuPlugin({ pluginKey: new PluginKey('myFloatingMenu'), editor, element: menuEl, shouldShow, offset: 0, keymap: { enterMenu: ['Alt-F10', 'Mod-/'] }, requireExplicitTrigger: false,});Useful when you need to instantiate the plugin yourself with a custom key (e.g. multiple editors on one page).
Styling
Section titled “Styling”The @domternal/theme package includes styles for .dm-floating-menu.
| Class | Description |
|---|---|
.dm-floating-menu | Container (absolutely positioned, hidden by default, visible via [data-show]) |
.dm-floating-menu[data-show] | Visible state |
.dm-floating-menu-group | Group container (flex column, gap 0.125rem) |
.dm-floating-menu-group-label | Section heading (uppercase, 0.6875rem, 600 weight) |
.dm-floating-menu-item | Button-like row (flex, padding 0.3125rem 0.5rem, roving tabindex target) |
.dm-floating-menu-item-icon | Icon span (1.25rem square) |
.dm-floating-menu-item-label | Label (flex 1, ellipsis on overflow) |
.dm-floating-menu-item-shortcut | Shortcut chip (code font, 0.6875rem) |
Visibility uses visibility: hidden / visible + opacity: 0 / 1 controlled by the data-show attribute on the root. Container background is opaque (not translucent) so nested content is fully occluded.
Accessibility
Section titled “Accessibility”role="menu"+aria-label="Floating menu"on the rootrole="menuitem"on each item- Roving tabindex - only the focused item is in Tab order
- Active item gets
aria-currentoraria-selected(framework-dependent) - Keyboard nav (ArrowDown/Up/Home/End/Enter/Space/Escape)
prefers-reduced-motionreduces animations
Exports
Section titled “Exports”// From @domternal/extension-block-menu (the extension + plugin):import { FloatingMenu, createFloatingMenuPlugin, floatingMenuPluginKey, showFloatingMenu, hideFloatingMenu,} from '@domternal/extension-block-menu';import type { FloatingMenuOptions, CreateFloatingMenuPluginOptions, FloatingMenuKeymap,} from '@domternal/extension-block-menu';
// From @domternal/core (the controller + types):import { FloatingMenuController, FLOATING_MENU_NO_FOCUS, groupFloatingMenuItems,} from '@domternal/core';import type { FloatingMenuItem, FloatingMenuItemsOverride, FloatingMenuGroup,} from '@domternal/core';Source
Section titled “Source”@domternal/extension-block-menu - FloatingMenu.ts
@domternal/core - FloatingMenuController.ts