Visual feedback for selections and cursor positions when editor loses focus.
The fastest way to add cursor overlay functionality is with the CursorOverlayKit, which includes the pre-configured CursorOverlayPlugin and the CursorOverlay UI component.
'use client';
import { AIChatPlugin } from '@platejs/ai/react';
import {
type CursorData,
type CursorOverlayState,
CursorOverlayPlugin,
useCursorOverlayPositions,
} from '@platejs/cursor';
import { BaseTablePlugin } from '@platejs/table';
import { RangeApi } from 'platejs';
import { useEditor, usePlateValue, usePluginStore } from 'platejs/react';
import * as React from 'react';
import { cn } from '@/lib/utils';
export function CursorOverlay() {
const containerRef = usePlateValue('containerRef');
const cursorStates = usePluginStore(CursorOverlayPlugin, 'cursors');
const { cursors } = useCursorOverlayPositions({
containerRef,
cursors: cursorStates,
});
return (
<>
{cursors.map((cursor) => (
<Cursor key={cursor.id} {...cursor} />
))}
</>
);
}
function Cursor({
id,
caretPosition,
data,
selection,
selectionRects,
}: CursorOverlayState<CursorData>) {
const editor = useEditor();
const streaming = usePluginStore(AIChatPlugin, 'streaming');
const { style, selectionStyle = style } = data ?? {};
const isCursor = selection ? RangeApi.isCollapsed(selection) : false;
if (streaming) return null;
// Skip overlay for multi-cell table selection (table has its own selection UI)
if (id === 'selection' && selection) {
const cellEntries =
editor.plugin(BaseTablePlugin).read.selection(selection)?.cellEntries ??
[];
if (cellEntries.length > 1) {
return null;
}
}
return (
<>
{selectionRects.map((position, i) => (
<div
key={i}
className={cn(
'pointer-events-none absolute z-10',
id === 'selection' && 'bg-brand/25',
id === 'selection' && isCursor && 'bg-primary'
)}
style={{
...selectionStyle,
...position,
}}
/>
))}
{caretPosition && (
<div
className={cn(
'pointer-events-none absolute z-10 w-0.5',
id === 'drag' && 'w-px bg-brand'
)}
style={{ ...caretPosition, ...style }}
/>
)}
</>
);
}
export const CursorOverlayKit = [
CursorOverlayPlugin.configure({
render: {
afterEditable: () => <CursorOverlay />,
},
}),
] as const;'use client';
import { AIChatPlugin } from '@platejs/ai/react';
import {
type CursorData,
type CursorOverlayState,
CursorOverlayPlugin,
useCursorOverlayPositions,
} from '@platejs/cursor';
import { BaseTablePlugin } from '@platejs/table';
import { RangeApi } from 'platejs';
import { useEditor, usePlateValue, usePluginStore } from 'platejs/react';
import * as React from 'react';
import { cn } from '@/lib/utils';
export function CursorOverlay() {
CursorOverlay: Renders cursor and selection overlays.import { createPlateEditor } from 'platejs/react';
import { CursorOverlayKit } from '@/components/editor/cursor-overlay';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
...CursorOverlayKit,
],
});import { createPlateEditor } from 'platejs/react';
import { CursorOverlayKit } from '@/components/editor/cursor-overlay';
const editor =
import { CursorOverlayPlugin } from '@platejs/cursor';
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
CursorOverlayPlugin,
],
});import { CursorOverlayPlugin } from '@platejs/cursor';
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
CursorOverlayPlugin,
],
});Configure the cursor overlay with a component to render overlays:
import { CursorOverlayPlugin } from '@platejs/cursor';
import { CursorOverlay } from '@/components/editor/cursor-overlay';
CursorOverlayPlugin.configure({
render: {
afterEditable: () => <CursorOverlay />,
},
});import { CursorOverlayPlugin } from '@platejs/cursor';
import { CursorOverlay } from '@/components/editor/cursor-overlay';
CursorOverlayPlugin.configure({
render.afterEditable: Assigns CursorOverlay to render after the editable content.The cursor overlay requires a container component to ensure correct positioning. If you're using the Editor component, this is handled automatically through EditorContainer.
For custom setups, ensure your editor is wrapped with a container that has the editor's unique ID:
import { PlateContainer } from 'platejs/react';
export function EditorContainer(props: React.HTMLAttributes<HTMLDivElement>) {
return <PlateContainer {...props} />;
}import { PlateContainer } from 'platejs/react';
export function EditorContainer(props: React.HTMLAttributes<HTMLDivElement>) {
return
To maintain the editor's selection state when focusing UI elements, add the data-plate-focus="true" attribute to those elements:
<ToolbarButton data-plate-focus="true">
{/* toolbar content */}
</ToolbarButton><ToolbarButton data-plate-focus="true">
{/* toolbar content */}
</ToolbarButton>This prevents the cursor overlay from disappearing when interacting with toolbar buttons or other UI elements.
Plugin that manages cursor and selection overlays for visual feedback.
Adds a cursor overlay with the specified key and state.
Removes a cursor overlay by its key.
Import useCursorOverlayPositions from @platejs/cursor. The hook calculates
cursor and selection rectangles for any cursor-state map.