Skip to main content

Overview

The FloatingMenu component renders a menu that appears when the cursor is on an empty line in the editor. It’s commonly used to provide quick access to block-level formatting options like headings, lists, or inserting media.

Type Signature

const FloatingMenu: React.ForwardRefExoticComponent<
  FloatingMenuProps & React.RefAttributes<HTMLDivElement>
>

type FloatingMenuProps = Omit<FloatingMenuPluginProps, 'element' | 'editor'> & {
  editor: Editor | null
  options?: FloatingMenuPluginProps['options']
} & React.HTMLAttributes<HTMLDivElement>

Props

editor
Editor | null
required
The editor instance. If not provided, the component will attempt to use the editor from React context via useCurrentEditor.
pluginKey
string | PluginKey
default:"'floatingMenu'"
Unique identifier for this floating menu plugin. Use different keys if you have multiple floating menus.
updateDelay
number
default:"250"
Delay in milliseconds before the menu position is updated. This debounces position updates for better performance.
resizeDelay
number
default:"60"
Delay in milliseconds before the menu position is updated on window resize. This debounces resize events for better performance.
shouldShow
function | null
default:"null"
Function that determines whether the menu should be shown. Receives editor state and selection information.
shouldShow?: (props: {
  editor: Editor
  view: EditorView
  state: EditorState
  oldState?: EditorState
  from: number
  to: number
}) => boolean
Default behavior: Shows when the cursor is in an empty text block at the root level and the editor has focus.
appendTo
HTMLElement | (() => HTMLElement)
The DOM element to append the menu to. Useful when you need to render the menu in a specific container for z-index or overflow reasons.Default: The editor’s parent element.
options
object
Floating UI configuration options for positioning and behavior.
children
React.ReactNode
required
The content to render inside the floating menu.
...props
React.HTMLAttributes<HTMLDivElement>
All standard HTML div attributes (className, style, etc.) are supported.

Usage Examples

Basic Usage

import { useEditor, EditorContent, FloatingMenu } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

function MyEditor() {
  const editor = useEditor({
    extensions: [StarterKit],
    content: '<p>Click on 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()}>
          List
        </button>
      </FloatingMenu>
    </>
  )
}

Block Type Selector

import { useEditor, EditorContent, FloatingMenu } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

function MyEditor() {
  const editor = useEditor({
    extensions: [StarterKit],
  })

  return (
    <>
      <EditorContent editor={editor} />
      <FloatingMenu
        editor={editor}
        className="floating-menu"
      >
        <button
          onClick={() => editor?.chain().focus().toggleHeading({ level: 1 }).run()}
        >
          Heading 1
        </button>
        <button
          onClick={() => editor?.chain().focus().toggleHeading({ level: 2 }).run()}
        >
          Heading 2
        </button>
        <button
          onClick={() => editor?.chain().focus().toggleBulletList().run()}
        >
          Bullet List
        </button>
        <button
          onClick={() => editor?.chain().focus().toggleOrderedList().run()}
        >
          Numbered List
        </button>
        <button
          onClick={() => editor?.chain().focus().toggleCodeBlock().run()}
        >
          Code Block
        </button>
      </FloatingMenu>
    </>
  )
}

Styled Floating Menu

import { useEditor, EditorContent, FloatingMenu } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

function MyEditor() {
  const editor = useEditor({
    extensions: [StarterKit],
  })

  return (
    <>
      <EditorContent editor={editor} />
      <FloatingMenu
        editor={editor}
        style={{
          background: '#1a1a1a',
          color: 'white',
          padding: '8px',
          borderRadius: '8px',
          boxShadow: '0 4px 12px rgba(0, 0, 0, 0.2)',
        }}
      >
        <div style={{ display: 'flex', gap: '4px' }}>
          <button
            style={{
              padding: '8px 12px',
              background: 'transparent',
              border: '1px solid #444',
              color: 'white',
              borderRadius: '4px',
              cursor: 'pointer',
            }}
            onClick={() => editor?.chain().focus().toggleHeading({ level: 1 }).run()}
          >
            H1
          </button>
          <button
            style={{
              padding: '8px 12px',
              background: 'transparent',
              border: '1px solid #444',
              color: 'white',
              borderRadius: '4px',
              cursor: 'pointer',
            }}
            onClick={() => editor?.chain().focus().toggleBulletList().run()}
          >
            List
          </button>
        </div>
      </FloatingMenu>
    </>
  )
}

Custom shouldShow Logic

import { useEditor, EditorContent, FloatingMenu } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

function MyEditor() {
  const editor = useEditor({
    extensions: [StarterKit],
  })

  return (
    <>
      <EditorContent editor={editor} />
      <FloatingMenu
        editor={editor}
        shouldShow={({ state, editor }) => {
          const { $anchor } = state.selection
          
          // Only show in paragraph nodes
          if ($anchor.parent.type.name !== 'paragraph') {
            return false
          }
          
          // Only show when empty and at root level
          return (
            $anchor.parent.childCount === 0 &&
            $anchor.depth === 1 &&
            editor.isEditable
          )
        }}
      >
        <button onClick={() => editor?.chain().focus().toggleHeading({ level: 1 }).run()}>
          H1
        </button>
      </FloatingMenu>
    </>
  )
}

Slash Command Style

import { useEditor, EditorContent, FloatingMenu } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { useState } from 'react'

function MyEditor() {
  const [showSlashMenu, setShowSlashMenu] = useState(false)
  
  const editor = useEditor({
    extensions: [StarterKit],
    onUpdate: ({ editor }) => {
      const { $from } = editor.state.selection
      const text = $from.parent.textContent
      setShowSlashMenu(text === '/')
    },
  })

  const insertBlock = (type: string) => {
    if (!editor) return
    
    // Delete the slash
    editor.chain().focus().deleteRange({ from: editor.state.selection.from - 1, to: editor.state.selection.from }).run()
    
    // Insert the block
    switch (type) {
      case 'h1':
        editor.chain().toggleHeading({ level: 1 }).run()
        break
      case 'list':
        editor.chain().toggleBulletList().run()
        break
      case 'code':
        editor.chain().toggleCodeBlock().run()
        break
    }
  }

  return (
    <>
      <EditorContent editor={editor} />
      {showSlashMenu && (
        <FloatingMenu
          editor={editor}
          options={{ placement: 'bottom-start' }}
        >
          <div className="slash-menu">
            <button onClick={() => insertBlock('h1')}>Heading 1</button>
            <button onClick={() => insertBlock('list')}>Bullet List</button>
            <button onClick={() => insertBlock('code')}>Code Block</button>
          </div>
        </FloatingMenu>
      )}
    </>
  )
}

With Icons

import { useEditor, EditorContent, FloatingMenu } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

function MyEditor() {
  const editor = useEditor({
    extensions: [StarterKit],
  })

  return (
    <>
      <EditorContent editor={editor} />
      <FloatingMenu editor={editor} className="floating-menu">
        <button
          onClick={() => editor?.chain().focus().toggleHeading({ level: 1 }).run()}
          title="Heading 1"
        >
          <svg width="16" height="16" viewBox="0 0 16 16">
            <path d="M2 3h2v10H2V3zm4 0h2v10H6V3zm6 0h2v10h-2V3z" />
          </svg>
        </button>
        <button
          onClick={() => editor?.chain().focus().toggleBulletList().run()}
          title="Bullet List"
        >
          <svg width="16" height="16" viewBox="0 0 16 16">
            <circle cx="2" cy="4" r="1" />
            <circle cx="2" cy="8" r="1" />
            <circle cx="2" cy="12" r="1" />
            <line x1="5" y1="4" x2="14" y2="4" />
            <line x1="5" y1="8" x2="14" y2="8" />
            <line x1="5" y1="12" x2="14" y2="12" />
          </svg>
        </button>
      </FloatingMenu>
    </>
  )
}

Custom Positioning

import { useEditor, EditorContent, FloatingMenu } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

function MyEditor() {
  const editor = useEditor({
    extensions: [StarterKit],
  })

  return (
    <>
      <EditorContent editor={editor} />
      <FloatingMenu
        editor={editor}
        options={{
          placement: 'left-start',
          offset: 12,
          shift: { padding: 8 },
          onShow: () => console.log('Menu shown'),
          onHide: () => console.log('Menu hidden'),
        }}
      >
        <div>+ Add block</div>
      </FloatingMenu>
    </>
  )
}

Styling

CSS Example

.floating-menu {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 4px;
  background: white;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.floating-menu button {
  padding: 8px 12px;
  border: none;
  background: transparent;
  text-align: left;
  cursor: pointer;
  border-radius: 4px;
  transition: background 0.2s;
}

.floating-menu button:hover {
  background: #f1f5f9;
}

.floating-menu button:active {
  background: #e2e8f0;
}

Notion-Style Plus Button

.floating-menu {
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: white;
  border: 1px solid #e2e8f0;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.2s;
}

.floating-menu:hover {
  background: #f1f5f9;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.floating-menu::before {
  content: '+';
  font-size: 18px;
  font-weight: bold;
  color: #64748b;
}

Important Notes

The FloatingMenu uses Floating UI for positioning, which provides intelligent placement that avoids overflow and adjusts to viewport constraints.
When clicking buttons inside the floating menu, make sure to use .focus() in your command chains to prevent the editor from losing focus, which would hide the menu.
The default shouldShow behavior only shows the menu on empty text blocks at the root level. Customize this behavior with the shouldShow prop to match your use case.

Default Behavior

By default, the floating menu:
  • Shows when the cursor is in an empty text block at root depth
  • Hides when text is typed
  • Hides when the editor loses focus
  • Positions itself to the right of the cursor
  • Only appears when the editor is editable
The menu will NOT show:
  • In empty list items
  • In code blocks
  • In nested content
  • When text exists in the block
  • When the editor doesn’t have focus

Comparison with BubbleMenu

FeatureFloatingMenuBubbleMenu
Appears onEmpty linesText selection
Default positionRight of cursorAbove selection
Common useBlock formattingText formatting
Example actionsInsert heading, list, mediaBold, italic, link

Build docs developers (and LLMs) love