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-elementComponents
<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
| Name | Type | Default | Description |
|---|---|---|---|
editor | Editor | null | undefined | The Tiptap editor instance to attach to |
shouldShow | boolean | undefined | Controls whether the floating element should be visible |
floatingOptions | Partial<UseFloatingOptions> | undefined | Additional options to pass to the floating UI |
zIndex | number | 50 | Z-index for the floating element |
onOpenChange | (open: boolean) => void | undefined | Callback fired when the visibility state changes |
referenceElement | HTMLElement | null | undefined | Reference element to position the floating element relative to. If provided, this takes precedence over getBoundingClientRect |
getBoundingClientRect | (editor: Editor) => DOMRect | null | getSelectionBoundingRect | Custom function to determine the position of the floating element. Only used if referenceElement is not provided |
closeOnEscape | boolean | true | Whether to close the floating element when Escape key is pressed |
children | React.ReactNode | undefined | Content 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 instanceselection(optional) - The selection to validate. Defaults toeditor.state.selectionexcludedNodeTypes(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 instanceelement- 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')
}
}