Skip to main content

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
}>

Props

editor
Editor
required
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.
options
TippyOptions
default:"{}"
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.
shouldShow
function
default:"null"
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>

Styled Floating Menu

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

Insert Media Menu

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

Multiple Floating Menus

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

Notes

  • 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

Build docs developers (and LLMs) love