Skip to main content
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

element
HTMLElement
required
The DOM element that contains your menu.Default: null
pluginKey
PluginKey | string
The plugin key for the floating menu.Default: 'floatingMenu'
updateDelay
number
The delay in milliseconds before the menu position should be updated. This can be useful to prevent performance issues.Default: 250
resizeDelay
number
The delay in milliseconds before the menu position should be updated on window resize.Default: 60
shouldShow
function
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 element
FloatingMenu.configure({
  element: menu,
  appendTo: () => document.body,
})
options
object
Configuration options passed to Floating UI for positioning. See Floating UI documentation for full details.Properties:
options.strategy
'absolute' | 'fixed'
Positioning strategy.Default: 'absolute'
options.placement
string
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
options.flip
object | boolean
Whether to flip the menu to the opposite side if there’s not enough space.Default: {}
options.shift
object | boolean
Whether to shift the menu along the axis to keep it in view.Default: {}
options.scrollTarget
HTMLElement | Window
The scrollable element that should be listened to when updating the position.Default: window
FloatingMenu.configure({
  element: menu,
  options: {
    scrollTarget: document.querySelector('.scroll-container'),
  },
})
options.onShow
() => void
Callback fired when the menu is shown.
options.onHide
() => void
Callback fired when the menu is hidden.
options.onUpdate
() => void
Callback fired when the menu position is updated.
options.onDestroy
() => void
Callback fired when the menu is destroyed.

Commands

updateFloatingMenuPosition
command
Manually update the position of the floating menu.
editor.commands.updateFloatingMenuPosition()

React Component Props

When using the React component, you can pass these props:
editor
Editor
The editor instance. Can be omitted if used inside EditorProvider.
pluginKey
string
The plugin key.Default: 'floatingMenu'
updateDelay
number
Update delay in milliseconds.
resizeDelay
number
Resize delay in milliseconds.
shouldShow
function
Function to determine visibility.
options
object
Floating UI configuration options.
Plus all standard HTML div attributes (className, style, etc.).

Vue Component Props

editor
Editor
required
The editor instance.
pluginKey
string
The plugin key.Default: 'floatingMenu'
updateDelay
number
Update delay in milliseconds.
resizeDelay
number
Resize delay in milliseconds.
shouldShow
function
Function to determine visibility.
options
object
Floating UI configuration options.
appendTo
HTMLElement | Function
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'],
    },
  },
})

Multiple Floating Menus

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:

Build docs developers (and LLMs) love