Floating Element

A floating UI element that positions itself relative to the current selection in a Tiptap editor. Used for floating toolbars, menus, and other UI elements that need to appear near the text cursor with intelligent positioning and interaction handling.

Installation

Add the component via the Tiptap CLI:

npx @tiptap/cli@latest add floating-element

Components

<FloatingElement />

A versatile React component that creates floating UI elements positioned relative to text selections in Tiptap editors.

Usage

import * as React from 'react'
import { EditorContent, EditorContext, useEditor } from '@tiptap/react'

// --- Tiptap Core Extensions ---
import { StarterKit } from '@tiptap/starter-kit'

// --- Tiptap UI ---
import { FloatingElement } from '@/components/tiptap-ui-utils/floating-element'
import { MarkButton } from '@/components/tiptap-ui/mark-button'

// --- UI Primitives ---
import { ButtonGroup } from '@/components/tiptap-ui-primitive/button'
import { Toolbar } from '@/components/tiptap-ui-primitive/toolbar'

// --- Tiptap Node ---
import '@/components/tiptap-node/paragraph-node/paragraph-node.scss'

export const FloatingElementExample = () => {
  const editor = useEditor({
    immediatelyRender: false,
    content: `<h2>Floating Element Example</h2>
      <p>Try selecting some text in this editor. A simple formatting toolbar will appear above your selection. 
      The FloatingElement component positions UI elements relative to the text selection or cursor position. 
      It's commonly used for contextual toolbars, menus, and other elements that should appear near the current editing context.</p>`,
    extensions: [StarterKit],
  })

  return (
    <EditorContext.Provider value={{ editor }}>
      <EditorContent editor={editor} role="presentation" />

      <FloatingElement editor={editor}>
        <Toolbar variant="floating">
          <ButtonGroup orientation="horizontal">
            <MarkButton type="bold" />
            <MarkButton type="italic" />
          </ButtonGroup>
        </Toolbar>
      </FloatingElement>
    </EditorContext.Provider>
  )
}

Props

NameTypeDefaultDescription
editorEditor | nullundefinedThe Tiptap editor instance to attach to
shouldShowbooleanundefinedControls whether the floating element should be visible
floatingOptionsPartial<UseFloatingOptions>undefinedAdditional options to pass to the floating UI
zIndexnumber50Z-index for the floating element
onOpenChange(open: boolean) => voidundefinedCallback fired when the visibility state changes
referenceElementHTMLElement | nullundefinedReference element to position the floating element relative to. If provided, this takes precedence over getBoundingClientRect
getBoundingClientRect(editor: Editor) => DOMRect | nullgetSelectionBoundingRectCustom function to determine the position of the floating element. Only used if referenceElement is not provided
closeOnEscapebooleantrueWhether to close the floating element when Escape key is pressed
childrenReact.ReactNodeundefinedContent to display inside the floating element

Advanced Usage Examples

Basic Floating Toolbar

import { shift, flip, offset } from '@floating-ui/react'
import { FloatingElement } from '@/components/tiptap-ui-utils/floating-element'

function FloatingToolbar({ editor }) {
  return (
    <FloatingElement
      editor={editor}
      floatingOptions={{
        placement: 'top',
        middleware: [shift(), flip(), offset(8)],
      }}
    >
      {/* Floating content here */}
    </FloatingElement>
  )
}

Custom Positioning with Mobile Support

import { FloatingElement } from '@/components/tiptap-ui-utils/floating-element'
import { useMobile } from '@/hooks/use-mobile'

function ResponsiveFloatingMenu({ editor, isMenuVisible }) {
  const isMobile = useMobile()

  const getCustomRect = (editor) => {
    // Custom positioning logic
    // Example: position relative to current cursor
    return editor.view.coordsAtPos(editor.state.selection.from)
  }

  return (
    <FloatingElement
      editor={editor}
      shouldShow={isMenuVisible}
      getBoundingClientRect={getCustomRect}
      {...(isMobile
        ? {
            style: {
              position: 'fixed',
              left: 0,
              right: 0,
              bottom: 0,
              margin: '.5rem',
              zIndex: 50,
            },
          }
        : {})}
    >
      {/* Floating content here */}
    </FloatingElement>
  )
}

Customize shouldShow Floating Menu

import { useState, useEffect } from 'react'
import { FloatingElement } from '@/components/tiptap-ui-utils/floating-element'
import { isSelectionValid } from '@/lib/tiptap-collab-utils'

function SelectionMenu({ editor }) {
  const [isVisible, setIsVisible] = useState(false)

  useEffect(() => {
    if (!editor) return

    const updateVisibility = () => {
      const hasSelection = !editor.state.selection.empty
      const isValidSelection = isSelectionValid(editor)
      setIsVisible(hasSelection && isValidSelection)
    }

    editor.on('selectionUpdate', updateVisibility)
    return () => editor.off('selectionUpdate', updateVisibility)
  }, [editor])

  return (
    <FloatingElement editor={editor} shouldShow={isVisible}>
      {/* Your floating content here */}
    </FloatingElement>
  )
}

Using Reference Element

Attach the floating element to a specific DOM element instead of the text selection:

import { useState } from 'react'
import { offset, flip, shift } from '@floating-ui/react'
import { FloatingElement } from '@/components/tiptap-ui-utils/floating-element'

function ButtonWithTooltip() {
  const [buttonRef, setButtonRef] = useState<HTMLElement | null>(null)
  const [showTooltip, setShowTooltip] = useState(false)

  return (
    <>
      <button
        ref={setButtonRef}
        onMouseEnter={() => setShowTooltip(true)}
        onMouseLeave={() => setShowTooltip(false)}
      >
        Hover me
      </button>

      <FloatingElement
        editor={editor}
        referenceElement={buttonRef}
        shouldShow={showTooltip}
        floatingOptions={{
          placement: 'top',
          middleware: [offset(8), flip(), shift()],
        }}
      >
        <div className="tooltip">Helpful tooltip content</div>
      </FloatingElement>
    </>
  )
}

Utilities

getSelectionBoundingRect(editor)

Gets the bounding rectangle of the current selection in the editor.

Parameters:

  • editor - The Tiptap editor instance

Returns: DOMRect | null - The bounding rectangle of the current selection

import { getSelectionBoundingRect } from '@/lib/tiptap-collab-utils'

const rect = getSelectionBoundingRect(editor)
console.log('Selection bounds:', rect)

isSelectionValid(editor, selection?, excludedNodeTypes?)

Checks if the current selection is valid for showing floating elements. Returns false for empty selections, code blocks, excluded node types, and table cells.

Parameters:

  • editor - The Tiptap editor instance
  • selection (optional) - The selection to validate. Defaults to editor.state.selection
  • excludedNodeTypes (optional) - Array of node type names to exclude. Defaults to ['imageUpload', 'horizontalRule']

Returns: boolean - true if the selection is valid for floating elements

import { isSelectionValid } from '@/lib/tiptap-collab-utils'

const shouldShow = isSelectionValid(editor)

// With custom excluded node types
const isValid = isSelectionValid(editor, undefined, ['image', 'video'])

isTextSelectionValid(editor)

Checks if the current text selection is valid for editing. Returns false for empty selections, code blocks, and node selections.

Parameters:

  • editor - The Tiptap editor instance

Returns: boolean - true if the text selection is valid

import { isTextSelectionValid } from '@/lib/tiptap-collab-utils'

const canEdit = isTextSelectionValid(editor)
if (canEdit) {
  // Show text editing toolbar
}

isElementWithinEditor(editor, element)

Checks if a DOM element is within the editor's DOM tree. Useful for determining click/focus events.

Parameters:

  • editor - The Tiptap editor instance
  • element - The DOM element to check

Returns: boolean - true if the element is within the editor

import { isElementWithinEditor } from '@/components/tiptap-ui-utils/floating-element'

const handleClick = (event: MouseEvent) => {
  if (isElementWithinEditor(editor, event.target as Node)) {
    console.log('Clicked inside editor')
  }
}