From zbeyens. The source code is available on GitHub.

Plate
PlatePliteEditorsTemplates
GitHub16kGitHub
DiscordDiscord
    • Stream
    • Copilot
  • Discussion
    • Comments
    • Suggestion
    • Basic Blocks
      • Blockquote
      • Heading
      • Horizontal Rule
    • Callout
    • Code Block
    • Column
    • Date
    • Equation
    • Link
    • List Classic
    • Media
    • MentionElement
    • Table
    • Table of Contents
    • Footnote
    • Toggle
  • Marks
    • Bold
    • Italic
    • Underline
    • Code
    • Highlight
    • Keyboard Input
    • Strikethrough
    • Subscript
    • Superscript
      • Font
      • Line Height
      • Text Align
    • Indent
    • List
      • Exit Break
      • Single Block
      • Trailing Block
    • Autoformat
    • Block Menu
    • Block Placeholder
    • Combobox
      • Emoji
      • MentionElement
      • Slash Command
    • Cursor Overlay
    • Drag & Drop
    • Navigation Feedback
    • Tabbable
    • Toolbar
    • Yjs
    • Multi SelectEditor
    • CSV
    • DOCX
    • HTML
    • Markdown

Cursor Overlay

PreviousNext

Visual feedback for selections and cursor positions when editor loses focus.

Cursor Overlay
Loading…
Slash CommandDrag & Drop

On This Page

FeaturesKit UsageInstallationAdd KitManual UsageInstallationAdd PluginConfigure PluginEditor Container SetupPreserving Selection FocusPluginsCursorOverlayPluginAPIeditor.plugin(CursorOverlayPlugin).api.addCursor(id, cursor)editor.plugin(CursorOverlayPlugin).api.removeCursor(id)HooksuseCursorOverlayPositions
Build your editor
Production-ready AI template and reusable components.
Get all-access

Features

  • Maintains selection highlight when another element is focused.
  • Dynamic selection display (e.g., during AI streaming).
  • Shows cursor position during drag operations.
  • Customizable styling for cursors and selections.
  • Essential for external UI interactions (e.g., link toolbar, AI combobox).
Report an issue

Kit Usage

Installation

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.

Add Kit

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 =




Manual Usage

Installation

pnpm add @platejs/cursor
pnpm add @platejs/cursor

Add Plugin

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 Plugin

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.

Editor Container Setup

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

Preserving Selection Focus

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.

Plugins

CursorOverlayPlugin

Plugin that manages cursor and selection overlays for visual feedback.

Options

    Object containing cursor states with their unique identifiers.

    • Default: {}

API

editor.plugin(CursorOverlayPlugin).api.addCursor(id, cursor)

Adds a cursor overlay with the specified key and state.

Parameters

    Unique identifier for the cursor (e.g., 'blur', 'drag', 'custom').

    The cursor state including selection and optional styling data.

editor.plugin(CursorOverlayPlugin).api.removeCursor(id)

Removes a cursor overlay by its key.

Parameters

    The cursor identifier to remove.

Hooks

useCursorOverlayPositions

Import useCursorOverlayPositions from @platejs/cursor. The hook calculates cursor and selection rectangles for any cursor-state map.

Optionsobject

    Minimum width in pixels for a selection rectangle. Useful for making cursor carets more visible.

    • Default: 1

    Whether to recalculate cursor positions when the container is resized.

    • Default: true

Returnsobject

    Array of cursor states with their corresponding selection rectangles and styling data.

    Function to manually trigger a recalculation of cursor positions.

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;
createPlateEditor
({
plugins: [
// ...otherPlugins,
...CursorOverlayKit,
],
});
render: {
afterEditable: () => <CursorOverlay />,
},
});
<
PlateContainer
{
...
props} />;
}