From zbeyens. The source code is available on GitHub.

Plate
PlatePliteEditorsTemplates
GitHub16kGitHub
DiscordDiscord
  • Plate
  • Plitev42
    • Editor API
    • Editor Transforms
    • Node
    • Element
    • Text
    • Path
    • Point
    • Range
    • Location
    • Location Ref
    • Document Change
  • Plate Core
    • Plate Components
    • Plate Editor
    • Plate Plugin
    • Plate Store
    • Plate Controller
  • Plate Utils
  • React Utils
  • cn
  • Floating
  • Resizable

Floating

PreviousNext

API reference for @platejs/floating.

@platejs/floating contains the React hooks and rectangle utilities used by floating toolbars, cursor-anchored UI, and virtual elements in Plate. It wraps Floating UI and exports the Floating UI primitives Plate components use.

Installation

pnpm add @platejs/floating
pnpm add @platejs/floating

Ownership

SurfaceOwnerUse
cnResizable

On This Page

InstallationOwnershipVirtual FloatingFloating ToolbarRectangle UtilitiesFloating UI Re-exportsRelated Components
Build your editor
Production-ready AI template and reusable components.
Get all-access
useVirtualFloating@platejs/floatinguseFloating with a controlled virtual reference element.
floating-toolbarCopied registry UICoordinates toolbar visibility, positioning, DOM refs, and outside clicks.
Rect utilities@platejs/floatingConvert editor ranges, DOM selection, and client rect arrays into Floating UI-compatible rects.
Floating UI exports@platejs/floatingRe-exported middleware and hooks from @floating-ui/react.

Virtual Floating

useVirtualFloating creates a Floating UI virtual reference. Use it when the floating element follows a selection, cursor, or computed rectangle instead of a real DOM reference element.

Virtual floating element
import {
  flip,
  getDefaultBoundingClientRect,
  offset,
  useVirtualFloating,
} from '@platejs/floating';
 
export function SelectionPopover({
  open,
  rect,
}: {
  open: boolean;
  rect?: DOMRect;
}) {
  const floating = useVirtualFloating({
    getBoundingClientRect: () => rect ?? getDefaultBoundingClientRect(),
    middleware: [offset(8), flip()],
    open,
    placement: 'top',
  });
 
  return (
    <div ref={floating.refs.setFloating} style={floating.style}>
      Selection actions
    </div>
  );
}
Virtual floating element
import {
  flip,
  getDefaultBoundingClientRect,
  offset,
  useVirtualFloating,
} from '@platejs/floating';
 
export function SelectionPopover({
  open,
  rect,
}: {
  open: boolean;
  rect?: DOMRect;
}) {
  const floating = useVirtualFloating({
    getBoundingClientRect: () => rect ?? getDefaultBoundingClientRect(),
    middleware: [offset(8), flip()],
    open,
    placement: 







OptionsUseVirtualFloatingOptions

    Supplies the virtual element rect. Defaults to getDefaultBoundingClientRect.

    When false, the returned style sets display: 'none'.

    Defaults to Floating UI autoUpdate.

    Forwarded to Floating UI useFloating.

ReturnsUseVirtualFloatingReturn

    Absolute/fixed position style: position, left, top, display, and visibility.

    Mutable virtual reference element. The hook updates its getBoundingClientRect.

    Floating UI refs. Attach refs.setFloating to the floating element.

    Floating UI manual position update.

Floating Toolbar

The copied floating-toolbar component owns editor focus, selection, read-only policy, outside clicks, and toolbar positioning. Copy it when your product needs that complete interaction; use useVirtualFloating directly for a different floating surface.

'use client';
 
import { AIChatPlugin } from '@platejs/ai/react';
import {
  BoldPlugin,
  CodePlugin,
  ItalicPlugin,
  StrikethroughPlugin,
  UnderlinePlugin,
} from '@platejs/basic-nodes/react';
import {
  type UseVirtualFloatingOptions,
  flip,
  getSelectionBoundingClientRect,
  offset,
  useVirtualFloating,
} from '@platejs/floating';
import { useComposedRef } from '@udecode/cn';
import { useOnClickOutside } from '@udecode/react-utils';
import { mergeProps } from '@udecode/utils';
import {
  BoldIcon,
  Code2Icon,
  ItalicIcon,
  StrikethroughIcon,
  UnderlineIcon,
  WandSparklesIcon,
} from 'lucide-react';
import {
  useEditorReadOnly,
  definePlatePlugin,
  useEditor,
  useEditorId,
  useEditorSelector,
  useEventEditorValue,
  usePluginStore,
} from 'platejs/react';
import * as React from 'react';
 
import { cn } from '@/lib/utils';
import { ToolbarGroup, Toolbar } from '@/components/editor/toolbar';
 
import { AIToolbarButton } from './ai-toolbar-button';
import { CommentToolbarButton } from './comment-toolbar-button';
import { InlineEquationToolbarButton } from './equation-toolbar-button';
import { linkPlugin } from './link';
import { LinkToolbarButton } from './link-toolbar-button';
import { MarkToolbarButton } from './mark-toolbar-button';
import { MoreToolbarButton } from './more-toolbar-button';
import { SuggestionToolbarButton } from './suggestion-toolbar-button';
import { TurnIntoToolbarButton } from './turn-into-toolbar-button';
 
export function FloatingToolbarButtons() {
  const readOnly = useEditorReadOnly();
 
  return (
    <>
      {!readOnly && (
        <>
          <ToolbarGroup>
            <AIToolbarButton tooltip="AI commands">
              <WandSparklesIcon />
              Ask AI
            </AIToolbarButton>
          </ToolbarGroup>
 
          <ToolbarGroup>
            <TurnIntoToolbarButton />
 
            <MarkToolbarButton plugin={BoldPlugin} tooltip="Bold (⌘+B)">
              <BoldIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton plugin={ItalicPlugin} tooltip="Italic (⌘+I)">
              <ItalicIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton
              plugin={UnderlinePlugin}
              tooltip="Underline (⌘+U)"
            >
              <UnderlineIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton
              plugin={StrikethroughPlugin}
              tooltip="Strikethrough (⌘+⇧+M)"
            >
              <StrikethroughIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton plugin={CodePlugin} tooltip="Code (⌘+E)">
              <Code2Icon />
            </MarkToolbarButton>
 
            <InlineEquationToolbarButton />
 
            <LinkToolbarButton />
          </ToolbarGroup>
        </>
      )}
 
      <ToolbarGroup>
        <CommentToolbarButton />
        <SuggestionToolbarButton />
 
        {!readOnly && <MoreToolbarButton />}
      </ToolbarGroup>
    </>
  );
}
 
type FloatingToolbarOptions = {
  floatingOptions?: UseVirtualFloatingOptions;
  hideToolbar?: boolean;
  showWhenReadOnly?: boolean;
};
 
type FloatingToolbarProps = React.ComponentProps<typeof Toolbar> & {
  options?: FloatingToolbarOptions;
};
 
export function FloatingToolbar(props: FloatingToolbarProps) {
  const editorId = useEditorId();
  const hasNodeSelection = useEditorSelector(
    (editor) => editor.read.selection.nodes().length > 0,
    { id: editorId }
  );
 
  if (hasNodeSelection) return null;
 
  return <TextFloatingToolbar {...props} />;
}
 
function TextFloatingToolbar({
  children,
  className,
  options,
  ...props
}: FloatingToolbarProps) {
  const editorId = useEditorId();
  const focusedEditorId = useEventEditorValue('focus');
  const isFloatingLinkOpen = !!usePluginStore(linkPlugin, 'mode');
  const isAIChatOpen = usePluginStore(AIChatPlugin, 'open');
  const editor = useEditor({ id: editorId });
  const selectionExpanded = useEditorSelector(
    (innerEditor) => innerEditor.read.selection.isExpanded(),
    { id: editorId }
  );
  const selectionText = useEditorSelector(
    (innerEditor2) => innerEditor2.read.text.string(),
    { id: editorId }
  );
  const selectionRange = useEditorSelector(
    (innerEditor3) => innerEditor3.read.selection(),
    { id: editorId }
  );
  const waitForCollapsedSelection = useEditorSelector(
    (innerEditor4, previous = false) => {
      if (!innerEditor4.read.selection.isExpanded()) return false;
      if (editorId !== focusedEditorId) return true;
 
      return previous;
    },
    { id: editorId }
  );
  const readOnly = useEditorReadOnly();
  const [dismissedSelection, setDismissedSelection] =
    React.useState<typeof selectionRange>(null);
  const [mouseDownOpen, setMouseDownOpen] = React.useState<boolean | null>(
    null
  );
  const [ownedOverlayOpen, setOwnedOverlayOpen] = React.useState(false);
  const open =
    selectionExpanded &&
    !!selectionText &&
    (editorId === focusedEditorId || ownedOverlayOpen) &&
    !isFloatingLinkOpen &&
    !isAIChatOpen &&
    !options?.hideToolbar &&
    (!readOnly || !!options?.showWhenReadOnly) &&
    (!waitForCollapsedSelection || readOnly || ownedOverlayOpen) &&
    mouseDownOpen !== false &&
    dismissedSelection !== selectionRange;
  const floating = useVirtualFloating(
    mergeProps<UseVirtualFloatingOptions>(
      {
        open,
        getBoundingClientRect: () => getSelectionBoundingClientRect(editor),
        onOpenChange: (nextOpen) => {
          setDismissedSelection(nextOpen ? null : selectionRange);
        },
      },
      {
        middleware: [
          offset(12),
          flip({
            fallbackPlacements: [
              'top-start',
              'top-end',
              'bottom-start',
              'bottom-end',
            ],
            padding: 12,
          }),
        ],
        placement: 'top',
        ...options?.floatingOptions,
      }
    )
  );
  const openStateRef = React.useRef(open);
 
  React.useEffect(() => {
    openStateRef.current = open;
  }, [open]);
 
  React.useEffect(() => {
    const onMouseUp = () => {
      setMouseDownOpen(null);
    };
    const onMouseDown = () => {
      setMouseDownOpen(openStateRef.current);
    };
 
    document.addEventListener('mouseup', onMouseUp);
    document.addEventListener('mousedown', onMouseDown);
 
    return () => {
      document.removeEventListener('mouseup', onMouseUp);
      document.removeEventListener('mousedown', onMouseDown);
    };
  }, []);
 
  const editorVersion = useEditorSelector(
    (innerEditor5) => innerEditor5.read.lastCommit()?.version ?? 0,
    { id: editorId }
  );
  const updateFloating = floating.update;
 
  React.useEffect(() => {
    if (open) updateFloating?.();
  }, [editorVersion, open, updateFloating]);
 
  const clickOutsideRef = useOnClickOutside(
    () => {
      setDismissedSelection(selectionRange);
    },
    { ignoreClass: 'ignore-click-outside/toolbar' }
  );
 
  const ref = useComposedRef<HTMLDivElement>(
    props.ref,
    floating.refs.setFloating
  );
 
  if (!open) return null;
 
  return (
    <div ref={clickOutsideRef}>
      <Toolbar
        {...props}
        ref={ref}
        onOverlayOpenChange={setOwnedOverlayOpen}
        style={floating.style}
        className={cn(
          'scrollbar-hide absolute z-50 overflow-x-auto whitespace-nowrap rounded-md border bg-popover p-1 opacity-100 shadow-md print:hidden',
          'max-w-[80vw]',
          className
        )}
      >
        {children}
      </Toolbar>
    </div>
  );
}
 
export const FloatingToolbarPlugin = definePlatePlugin('floatingToolbar', {
  render: {
    afterEditable: () => (
      <FloatingToolbar>
        <FloatingToolbarButtons />
      </FloatingToolbar>
    ),
  },
});
 
export const FloatingToolbarKit = [FloatingToolbarPlugin] as const;
'use client';
 
import { AIChatPlugin } from '@platejs/ai/react';
import {
  BoldPlugin,
  CodePlugin,
  ItalicPlugin,
  StrikethroughPlugin,
  UnderlinePlugin,
} from '@platejs/basic-nodes/react';
import {
  type UseVirtualFloatingOptions,
  flip,
  getSelectionBoundingClientRect,
  offset,
  useVirtualFloating,
} from '@platejs/floating';
import { useComposedRef } from '@udecode/cn';
import { useOnClickOutside } from '@udecode/react-utils';
import { mergeProps } from '@udecode/utils';











































































































































































































































































Rectangle Utilities

These helpers normalize editor locations and DOM ranges into rectangles.

Methods

    Returns a zero-size offscreen rect used as a safe Floating UI fallback.

    Creates a Floating UI virtual element with getDefaultBoundingClientRect.

    Creates a ref-like object whose current.getBoundingClientRect() reads editor locations. It throws when no rect exists and no fallbackRect is provided.

    Reads one or more editor locations, converts them to DOM ranges, and returns the merged bounding rect. If at is omitted, it uses editor.selection.

    Returns the DOM rect for a range, or getDefaultBoundingClientRect() when the range or DOM range is missing.

    Returns the selection rect only when the editor selection is expanded. Collapsed selections return the default rect.

    Returns window.getSelection().getRangeAt(0).getBoundingClientRect(), or the default rect when no DOM selection exists.

    Creates a DOMRect-like object and computes width, height, x, and y.

    Merges client rects by min left/top and max right/bottom. It throws when the array is empty.

Floating UI Re-exports

@platejs/floating re-exports the Floating UI middleware and React hooks used by Plate UI, including autoUpdate, flip, hide, inline, offset, shift, size, useFloating, useInteractions, useClick, useDismiss, FloatingPortal, and related types.

Use those exports when a Plate UI component already imports from @platejs/floating; use @floating-ui/react directly only when the component is not coupled to Plate.

Related Components

  • Toolbar covers the registry floating-toolbar component that consumes these hooks.
  • Plate Store covers useEditorId and useEventEditorValue.
'top'
,
});
return (
<div ref={floating.refs.setFloating} style={floating.style}>
Selection actions
</div>
);
}
import
{
BoldIcon,
Code2Icon,
ItalicIcon,
StrikethroughIcon,
UnderlineIcon,
WandSparklesIcon,
} from 'lucide-react';
import {
useEditorReadOnly,
definePlatePlugin,
useEditor,
useEditorId,
useEditorSelector,
useEventEditorValue,
usePluginStore,
} from 'platejs/react';
import * as React from 'react';
import { cn } from '@/lib/utils';
import { ToolbarGroup, Toolbar } from '@/components/editor/toolbar';
import { AIToolbarButton } from './ai-toolbar-button';
import { CommentToolbarButton } from './comment-toolbar-button';
import { InlineEquationToolbarButton } from './equation-toolbar-button';
import { linkPlugin } from './link';
import { LinkToolbarButton } from './link-toolbar-button';
import { MarkToolbarButton } from './mark-toolbar-button';
import { MoreToolbarButton } from './more-toolbar-button';
import { SuggestionToolbarButton } from './suggestion-toolbar-button';
import { TurnIntoToolbarButton } from './turn-into-toolbar-button';
export function FloatingToolbarButtons() {
const readOnly = useEditorReadOnly();
return (
<>
{!readOnly && (
<>
<ToolbarGroup>
<AIToolbarButton tooltip="AI commands">
<WandSparklesIcon />
Ask AI
</AIToolbarButton>
</ToolbarGroup>
<ToolbarGroup>
<TurnIntoToolbarButton />
<MarkToolbarButton plugin={BoldPlugin} tooltip="Bold (⌘+B)">
<BoldIcon />
</MarkToolbarButton>
<MarkToolbarButton plugin={ItalicPlugin} tooltip="Italic (⌘+I)">
<ItalicIcon />
</MarkToolbarButton>
<MarkToolbarButton
plugin={UnderlinePlugin}
tooltip="Underline (⌘+U)"
>
<UnderlineIcon />
</MarkToolbarButton>
<MarkToolbarButton
plugin={StrikethroughPlugin}
tooltip="Strikethrough (⌘+⇧+M)"
>
<StrikethroughIcon />
</MarkToolbarButton>
<MarkToolbarButton plugin={CodePlugin} tooltip="Code (⌘+E)">
<Code2Icon />
</MarkToolbarButton>
<InlineEquationToolbarButton />
<LinkToolbarButton />
</ToolbarGroup>
</>
)}
<ToolbarGroup>
<CommentToolbarButton />
<SuggestionToolbarButton />
{!readOnly && <MoreToolbarButton />}
</ToolbarGroup>
</>
);
}
type FloatingToolbarOptions = {
floatingOptions?: UseVirtualFloatingOptions;
hideToolbar?: boolean;
showWhenReadOnly?: boolean;
};
type FloatingToolbarProps = React.ComponentProps<typeof Toolbar> & {
options?: FloatingToolbarOptions;
};
export function FloatingToolbar(props: FloatingToolbarProps) {
const editorId = useEditorId();
const hasNodeSelection = useEditorSelector(
(editor) => editor.read.selection.nodes().length > 0,
{ id: editorId }
);
if (hasNodeSelection) return null;
return <TextFloatingToolbar {...props} />;
}
function TextFloatingToolbar({
children,
className,
options,
...props
}: FloatingToolbarProps) {
const editorId = useEditorId();
const focusedEditorId = useEventEditorValue('focus');
const isFloatingLinkOpen = !!usePluginStore(linkPlugin, 'mode');
const isAIChatOpen = usePluginStore(AIChatPlugin, 'open');
const editor = useEditor({ id: editorId });
const selectionExpanded = useEditorSelector(
(innerEditor) => innerEditor.read.selection.isExpanded(),
{ id: editorId }
);
const selectionText = useEditorSelector(
(innerEditor2) => innerEditor2.read.text.string(),
{ id: editorId }
);
const selectionRange = useEditorSelector(
(innerEditor3) => innerEditor3.read.selection(),
{ id: editorId }
);
const waitForCollapsedSelection = useEditorSelector(
(innerEditor4, previous = false) => {
if (!innerEditor4.read.selection.isExpanded()) return false;
if (editorId !== focusedEditorId) return true;
return previous;
},
{ id: editorId }
);
const readOnly = useEditorReadOnly();
const [dismissedSelection, setDismissedSelection] =
React.useState<typeof selectionRange>(null);
const [mouseDownOpen, setMouseDownOpen] = React.useState<boolean | null>(
null
);
const [ownedOverlayOpen, setOwnedOverlayOpen] = React.useState(false);
const open =
selectionExpanded &&
!!selectionText &&
(editorId === focusedEditorId || ownedOverlayOpen) &&
!isFloatingLinkOpen &&
!isAIChatOpen &&
!options?.hideToolbar &&
(!readOnly || !!options?.showWhenReadOnly) &&
(!waitForCollapsedSelection || readOnly || ownedOverlayOpen) &&
mouseDownOpen !== false &&
dismissedSelection !== selectionRange;
const floating = useVirtualFloating(
mergeProps<UseVirtualFloatingOptions>(
{
open,
getBoundingClientRect: () => getSelectionBoundingClientRect(editor),
onOpenChange: (nextOpen) => {
setDismissedSelection(nextOpen ? null : selectionRange);
},
},
{
middleware: [
offset(12),
flip({
fallbackPlacements: [
'top-start',
'top-end',
'bottom-start',
'bottom-end',
],
padding: 12,
}),
],
placement: 'top',
...options?.floatingOptions,
}
)
);
const openStateRef = React.useRef(open);
React.useEffect(() => {
openStateRef.current = open;
}, [open]);
React.useEffect(() => {
const onMouseUp = () => {
setMouseDownOpen(null);
};
const onMouseDown = () => {
setMouseDownOpen(openStateRef.current);
};
document.addEventListener('mouseup', onMouseUp);
document.addEventListener('mousedown', onMouseDown);
return () => {
document.removeEventListener('mouseup', onMouseUp);
document.removeEventListener('mousedown', onMouseDown);
};
}, []);
const editorVersion = useEditorSelector(
(innerEditor5) => innerEditor5.read.lastCommit()?.version ?? 0,
{ id: editorId }
);
const updateFloating = floating.update;
React.useEffect(() => {
if (open) updateFloating?.();
}, [editorVersion, open, updateFloating]);
const clickOutsideRef = useOnClickOutside(
() => {
setDismissedSelection(selectionRange);
},
{ ignoreClass: 'ignore-click-outside/toolbar' }
);
const ref = useComposedRef<HTMLDivElement>(
props.ref,
floating.refs.setFloating
);
if (!open) return null;
return (
<div ref={clickOutsideRef}>
<Toolbar
{...props}
ref={ref}
onOverlayOpenChange={setOwnedOverlayOpen}
style={floating.style}
className={cn(
'scrollbar-hide absolute z-50 overflow-x-auto whitespace-nowrap rounded-md border bg-popover p-1 opacity-100 shadow-md print:hidden',
'max-w-[80vw]',
className
)}
>
{children}
</Toolbar>
</div>
);
}
export const FloatingToolbarPlugin = definePlatePlugin('floatingToolbar', {
render: {
afterEditable: () => (
<FloatingToolbar>
<FloatingToolbarButtons />
</FloatingToolbar>
),
},
});
export const FloatingToolbarKit = [FloatingToolbarPlugin] as const;