Overview
The FloatingMenu component renders a menu that appears when the cursor is on an empty line. Itβs commonly used to provide quick access to block-level formatting options like headings, images, or other content types.
Signature
const FloatingMenu: DefineComponent<{
editor: Editor
pluginKey?: string | PluginKey
updateDelay?: number
resizeDelay?: number
options?: TippyOptions
appendTo?: Element | (() => Element) | 'parent'
shouldShow?: (props: {
editor: Editor
view: EditorView
state: EditorState
oldState?: EditorState
}) => boolean
}>
The Tiptap editor instance. This prop is required.
pluginKey
string | PluginKey
default:"'floatingMenu'"
A unique key for the ProseMirror plugin. Use this if you need multiple floating menus or want to identify this specific plugin.
updateDelay
number
default:"undefined"
Delay in milliseconds before updating the menu position after cursor movement. Useful for debouncing rapid cursor changes.
resizeDelay
number
default:"undefined"
Delay in milliseconds before updating the menu position after a window resize event.
Configuration options for the underlying Tippy.js positioning library. Use this to customize the menuβs placement, offset, animation, and other display properties.Common options:
placement - Position relative to reference (e.g., βtopβ, βbottomβ, βleftβ, βrightβ)
offset - Distance from the reference element
duration - Animation duration
zIndex - CSS z-index value
appendTo
Element | (() => Element) | 'parent'
default:"undefined"
The element to append the floating menu to. Can be:
- A DOM element
- A function that returns a DOM element
- The string
'parent' to append to the editorβs parent element
By default, the menu is appended to document.body.
A callback function that determines whether the floating menu should be visible. Receives an object with:
editor - The editor instance
view - The ProseMirror EditorView
state - Current editor state
oldState - Previous editor state
Return true to show the menu, false to hide it.By default, the menu shows when the cursor is in an empty paragraph and hides otherwise.
Lifecycle
- onMounted: Registers the floating menu plugin with the editor and sets up positioning
- onBeforeUnmount: Unregisters the plugin from the editor
Styling
The component inherits attributes (inheritAttrs: false is set, but attributes are manually applied), allowing you to add custom classes, styles, or other HTML attributes:
<FloatingMenu :editor="editor" class="custom-floating-menu" style="background: white;">
<!-- content -->
</FloatingMenu>
Examples
Basic Usage
<script setup>
import { useEditor, EditorContent, FloatingMenu } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
const editor = useEditor({
content: '<p>Click in this empty paragraph below to see the floating menu</p><p></p>',
extensions: [
StarterKit,
],
})
</script>
<template>
<div>
<EditorContent :editor="editor" />
<FloatingMenu :editor="editor" v-if="editor">
<button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()">
H1
</button>
<button @click="editor.chain().focus().toggleHeading({ level: 2 }).run()">
H2
</button>
<button @click="editor.chain().focus().toggleBulletList().run()">
Bullet List
</button>
</FloatingMenu>
</div>
</template>
<script setup>
import { useEditor, EditorContent, FloatingMenu } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
const editor = useEditor({
content: '<p>Start typing on a new line...</p>',
extensions: [StarterKit],
})
</script>
<template>
<div>
<EditorContent :editor="editor" />
<FloatingMenu :editor="editor" v-if="editor" class="floating-menu">
<button
@click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
>
H1
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
>
H2
</button>
<button
@click="editor.chain().focus().toggleBulletList().run()"
:class="{ 'is-active': editor.isActive('bulletList') }"
>
Bullet List
</button>
</FloatingMenu>
</div>
</template>
<style scoped>
.floating-menu {
display: flex;
gap: 0.25rem;
padding: 0.25rem;
background-color: white;
border: 1px solid #ccc;
border-radius: 0.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.floating-menu button {
padding: 0.25rem 0.5rem;
border: none;
background: transparent;
cursor: pointer;
border-radius: 0.25rem;
}
.floating-menu button:hover {
background-color: #f0f0f0;
}
.floating-menu button.is-active {
background-color: #000;
color: #fff;
}
</style>
Custom shouldShow Logic
<script setup>
import { useEditor, EditorContent, FloatingMenu } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
const editor = useEditor({
content: '<p>Custom visibility logic</p>',
extensions: [StarterKit],
})
// Only show the floating menu when in an empty paragraph that's not a heading
function shouldShow({ editor, state }) {
const { $from } = state.selection
const isEmptyParagraph = $from.parent.type.name === 'paragraph'
&& $from.parent.content.size === 0
const isHeading = editor.isActive('heading')
return isEmptyParagraph && !isHeading
}
</script>
<template>
<div>
<EditorContent :editor="editor" />
<FloatingMenu
:editor="editor"
v-if="editor"
:shouldShow="shouldShow"
>
<button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()">
Turn into H1
</button>
<button @click="editor.chain().focus().toggleBulletList().run()">
Turn into List
</button>
</FloatingMenu>
</div>
</template>
With Tippy Options
<script setup>
import { useEditor, EditorContent, FloatingMenu } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
const editor = useEditor({
content: '<p>Positioned floating menu</p>',
extensions: [StarterKit],
})
const tippyOptions = {
placement: 'left',
duration: 100,
offset: [0, 10],
zIndex: 1000,
}
</script>
<template>
<div>
<EditorContent :editor="editor" />
<FloatingMenu
:editor="editor"
v-if="editor"
:options="tippyOptions"
>
<div class="menu-content">
<button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()">
H1
</button>
<button @click="editor.chain().focus().toggleHeading({ level: 2 }).run()">
H2
</button>
</div>
</FloatingMenu>
</div>
</template>
<script setup>
import { useEditor, EditorContent, FloatingMenu } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import Image from '@tiptap/extension-image'
const editor = useEditor({
content: '<p>Insert media on empty lines</p>',
extensions: [
StarterKit,
Image,
],
})
function addImage() {
const url = window.prompt('Enter image URL')
if (url) {
editor.value?.chain().focus().setImage({ src: url }).run()
}
}
function addCodeBlock() {
editor.value?.chain().focus().toggleCodeBlock().run()
}
</script>
<template>
<div>
<EditorContent :editor="editor" />
<FloatingMenu :editor="editor" v-if="editor" class="floating-menu">
<button @click="addImage">
Add Image
</button>
<button @click="addCodeBlock">
Add Code Block
</button>
<button @click="editor.chain().focus().toggleBulletList().run()">
Add List
</button>
</FloatingMenu>
</div>
</template>
<script setup>
import { useEditor, EditorContent, FloatingMenu } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
const editor = useEditor({
content: '<p>Different menus for different contexts</p>',
extensions: [StarterKit],
})
function shouldShowTextMenu({ editor }) {
return editor.isActive('paragraph')
}
function shouldShowHeadingMenu({ editor }) {
return editor.isActive('heading')
}
</script>
<template>
<div>
<EditorContent :editor="editor" />
<!-- Text options menu -->
<FloatingMenu
:editor="editor"
v-if="editor"
pluginKey="textMenu"
:shouldShow="shouldShowTextMenu"
>
<button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()">
H1
</button>
<button @click="editor.chain().focus().toggleBulletList().run()">
List
</button>
</FloatingMenu>
<!-- Heading options menu -->
<FloatingMenu
:editor="editor"
v-if="editor"
pluginKey="headingMenu"
:shouldShow="shouldShowHeadingMenu"
>
<button @click="editor.chain().focus().setParagraph().run()">
Turn into Paragraph
</button>
</FloatingMenu>
</div>
</template>
- The floating menu is automatically positioned near the cursor using Tippy.js
- By default, it shows only when the cursor is in an empty paragraph
- The component renders as a
<div> element that you can style with classes or inline styles
- Multiple floating menus can coexist by using different
pluginKey values
- The menu is removed from the DOM and re-parented by the plugin when shown
- Use
shouldShow to customize when the menu appears based on editor state
- The floating menu is ideal for slash commands or block-level content insertion
See Also