The FloatingMenu extension provides a menu that appears when the cursor is in an empty line at the root level. It’s commonly used for slash commands, block insertion menus, or quick formatting options. The menu is positioned using Floating UI and supports custom visibility rules.
Installation
npm install @tiptap/extension-floating-menu
Basic Usage
Vanilla JavaScript
import { Editor } from '@tiptap/core'
import FloatingMenu from '@tiptap/extension-floating-menu'
const menu = document.querySelector('#floating-menu')
const editor = new Editor({
extensions: [
FloatingMenu.configure({
element: menu,
}),
],
})
React
import { FloatingMenu, useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
function Editor() {
const editor = useEditor({
extensions: [StarterKit],
content: '<p>Place cursor in an empty line to see the floating menu</p>',
})
return (
<>
<EditorContent editor={editor} />
<FloatingMenu editor={editor}>
<button onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}>
H1
</button>
<button onClick={() => editor.chain().focus().toggleBulletList().run()}>
Bullet List
</button>
</FloatingMenu>
</>
)
}
Vue
<template>
<editor-content :editor="editor" />
<floating-menu :editor="editor" v-if="editor">
<button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()">
H1
</button>
<button @click="editor.chain().focus().toggleBulletList().run()">
Bullet List
</button>
</floating-menu>
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import { FloatingMenu } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
export default {
components: {
EditorContent,
FloatingMenu,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [StarterKit],
content: '<p>Place cursor in an empty line to see the floating menu</p>',
})
},
beforeUnmount() {
this.editor.destroy()
},
}
</script>
Configuration Options
The DOM element that contains your menu.Default: null
The plugin key for the floating menu.Default: 'floatingMenu'
The delay in milliseconds before the menu position should be updated. This can be useful to prevent performance issues.Default: 250
The delay in milliseconds before the menu position should be updated on window resize.Default: 60
A function that determines whether the menu should be shown or not. The default implementation shows the menu when the cursor is in an empty text block at the root level.Parameters:
editor: The editor instance
view: The ProseMirror EditorView
state: The current editor state
oldState: The previous editor state (optional)
from: Selection start position
to: Selection end position
Default behavior:
- Shows when editor has focus
- Selection is empty (cursor, not range)
- Cursor is at root depth (depth === 1)
- Current block is empty text block
- Editor is editable
FloatingMenu.configure({
element: menu,
shouldShow: ({ editor, view, state }) => {
const { selection } = state
const { $anchor, empty } = selection
const isRootDepth = $anchor.depth === 1
const isEmptyTextBlock =
$anchor.parent.isTextblock &&
!$anchor.parent.textContent
return view.hasFocus() && empty && isRootDepth && isEmptyTextBlock
},
})
appendTo
HTMLElement | (() => HTMLElement)
The DOM element to append your menu to. Sometimes the menu needs to be appended to a different DOM context due to accessibility, clipping, or z-index issues.Default: Editor’s parent elementFloatingMenu.configure({
element: menu,
appendTo: () => document.body,
})
Configuration options passed to Floating UI for positioning. See Floating UI documentation for full details.Properties:Positioning strategy.Default: 'absolute'
Menu placement relative to cursor. Options: 'top', 'right', 'bottom', 'left', 'top-start', 'top-end', 'right-start', 'right-end', 'bottom-start', 'bottom-end', 'left-start', 'left-end'Default: 'right'
options.offset
number | object | boolean
Distance in pixels from the cursor.Default: 8
Whether to flip the menu to the opposite side if there’s not enough space.Default: {}
Whether to shift the menu along the axis to keep it in view.Default: {}
The scrollable element that should be listened to when updating the position.Default: windowFloatingMenu.configure({
element: menu,
options: {
scrollTarget: document.querySelector('.scroll-container'),
},
})
Callback fired when the menu is shown.
Callback fired when the menu is hidden.
Callback fired when the menu position is updated.
Callback fired when the menu is destroyed.
Commands
Manually update the position of the floating menu.editor.commands.updateFloatingMenuPosition()
React Component Props
When using the React component, you can pass these props:
The editor instance. Can be omitted if used inside EditorProvider.
The plugin key.Default: 'floatingMenu'
Update delay in milliseconds.
Resize delay in milliseconds.
Function to determine visibility.
Floating UI configuration options.
Plus all standard HTML div attributes (className, style, etc.).
Vue Component Props
The plugin key.Default: 'floatingMenu'
Update delay in milliseconds.
Resize delay in milliseconds.
Function to determine visibility.
Floating UI configuration options.
Element to append the menu to.
Advanced Examples
Slash Command Menu
import { FloatingMenu } from '@tiptap/react'
import { useState } from 'react'
function Editor() {
const [query, setQuery] = useState('')
const commands = [
{ name: 'Heading 1', command: () => editor.chain().focus().toggleHeading({ level: 1 }).run() },
{ name: 'Heading 2', command: () => editor.chain().focus().toggleHeading({ level: 2 }).run() },
{ name: 'Bullet List', command: () => editor.chain().focus().toggleBulletList().run() },
{ name: 'Numbered List', command: () => editor.chain().focus().toggleOrderedList().run() },
]
const filteredCommands = commands.filter(cmd =>
cmd.name.toLowerCase().includes(query.toLowerCase())
)
return (
<>
<EditorContent editor={editor} />
<FloatingMenu
editor={editor}
shouldShow={({ state }) => {
const { $anchor } = state.selection
const text = $anchor.parent.textContent
return text.startsWith('/')
}}
>
<div className="slash-menu">
{filteredCommands.map(cmd => (
<button key={cmd.name} onClick={cmd.command}>
{cmd.name}
</button>
))}
</div>
</FloatingMenu>
</>
)
}
Show in Nested Blocks
FloatingMenu.configure({
element: menu,
shouldShow: ({ view, state }) => {
const { selection } = state
const { $anchor, empty } = selection
// Allow any depth, not just root level
const isEmptyTextBlock =
$anchor.parent.isTextblock &&
!$anchor.parent.textContent
return view.hasFocus() && empty && isEmptyTextBlock
},
})
Update Position Programmatically
// Trigger a position update
editor.commands.updateFloatingMenuPosition()
// Or via transaction meta
editor.view.dispatch(
editor.state.tr.setMeta('floatingMenu', 'updatePosition')
)
Custom Positioning
FloatingMenu.configure({
element: menu,
options: {
placement: 'left',
offset: 16,
flip: {
fallbackPlacements: ['right', 'top', 'bottom'],
},
},
})
function Editor() {
const editor = useEditor({
extensions: [StarterKit],
})
return (
<>
<EditorContent editor={editor} />
{/* Block insertion menu */}
<FloatingMenu
editor={editor}
pluginKey="blockMenu"
>
<button onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}>
H1
</button>
<button onClick={() => editor.chain().focus().toggleBulletList().run()}>
List
</button>
</FloatingMenu>
{/* AI writing assistant */}
<FloatingMenu
editor={editor}
pluginKey="aiMenu"
options={{ placement: 'left' }}
>
<button onClick={() => improveWriting()}>
✨ Improve Writing
</button>
</FloatingMenu>
</>
)
}
Source Code
View the source code on GitHub: