Projection And Overlays

PreviousNext

Choose between decorations, projection sources, annotations, widgets, and render props.

Projection turns model ranges into renderable text slices and overlay anchors. Use this page to choose between Editable.decorate, provider-owned decoration sources, annotations, widgets, and render props.

Choose The Right Surface

Do not put every overlay into decorate. Pick the smallest owner that matches the lifetime of the UI.

NeedStart withOwner
One editable needs a simple highlightEditable.decorate@platejs/slate-react
Search, diagnostics, or external highlights are shared with other UIuseSlateDecorationSource or useSlateRangeDecorationSource plus <Slate decorationSources>@platejs/slate-react
A range has durable identityuseSlateAnnotationStore plus <Slate annotationStore>@platejs/slate-react
UI is anchored to a selection, node, or annotationuseSlateWidgetStore, useSlateWidgets, and useSlateWidget@platejs/slate-react
Text paint depends on projected slicesEditable renderSegment@platejs/slate-react
The overlay claim needs browser-visible proofscreenshot, DOM selection, displayed selection, and follow-up typing@platejs/browser

Use decorations for paint. Use annotations when a range needs identity. Use widgets when UI hangs off a node, selection, or annotation.

Runtime Pipeline

Projection is a React rendering layer, not a second document model.

StageWhat happensOwner
Sourcedecorate, decoration sources, annotation stores, and selection sources provide ranges.@platejs/slate-react
ProjectionSources are projected onto runtime text ids and split into slices.@platejs/slate-react
RenderEditable renders projected text through renderSegment, renderLeaf, renderText, and element renderers.@platejs/slate-react
Overlay UISidebars, popovers, toolbars, and widgets read annotation or widget snapshots.App UI plus @platejs/slate-react
Commit refreshEditor commits name the affected projection runtime ids when possible.@platejs/slate and @platejs/slate-react
ProofBrowser tests verify visible highlights, selection, focus, and follow-up editing.@platejs/browser

Keep document data in Slate nodes, roots, and document state. Keep overlay payloads small and render-facing.

Decorations

Use Editable.decorate for a local highlight that belongs to one editable.

<Editable
  decorate={([node, path]) => {
    if (!TextApi.isText(node)) return [];
 
    const start = node.text.indexOf(query);
 
    return start === -1
      ? []
      : [
          {
            anchor: { path, offset: start },
            data: { search: true },
            focus: { path, offset: start + query.length },
          },
        ];
  }}
  renderSegment={(segment, children) =>
    segment.slices.some((slice) => slice.data?.search) ? (
      <mark>{children}</mark>
    ) : (
      children
    )
  }
/>
<Editable
  decorate={([node, path]) => {
    if (!TextApi.isText(node)) return [];
 
    const start = node.text.indexOf(query);
 
    return start === -1
      ? []
      : [
          {
            anchor: { path, offset: start },
            data: { search: true },
            focus: { path, offset: start + query.length },
          },
        ];
  }}
  renderSegment={(segment, children) =>
    segment.slices.some((slice) => slice.data?.search) ? (
      <mark>{children}</mark>
    ) : (
      children
    )
  }
/>

Move to provider-owned decoration sources when ranges are shared with sidebars, toolbars, search panels, diagnostics, or other overlay UI.

const searchSource = useSlateRangeDecorationSource(editor, {
  deps: [query],
  id: "search",
  read: ({ snapshot }) => findSearchRanges(snapshot, query),
});
 
return (
  <Slate decorationSources={[searchSource]} editor={editor}>
    <Editable renderSegment={renderSearchMatch} />
  </Slate>
);
const searchSource = useSlateRangeDecorationSource(editor, {
  deps: [query],
  id: "search",
  read: ({ snapshot }) => findSearchRanges(snapshot, query),
});
 
return (
  <Slate decorationSources={[searchSource]} editor={editor}>
    <Editable renderSegment={renderSearchMatch} />
  </Slate>
);

Use decorateDirtiness and decorateRuntimeScope when a decoration callback depends on external projection state and can name which runtime targets should refresh.

Annotations

Annotations attach durable ids to ranges. They are the right owner for comments, suggestions, external diagnostics, and review markers.

const annotationStore = useSlateAnnotationStore(editor, {
  deps: [comments],
  project: () =>
    comments.map((comment) => ({
      anchor: comment.anchor,
      data: comment,
      id: comment.id,
      projection: { tone: comment.tone },
    })),
});
 
return (
  <Slate annotationStore={annotationStore} editor={editor}>
    <Editable renderSegment={renderCommentSegment} />
    <CommentsSidebar />
  </Slate>
);
const annotationStore = useSlateAnnotationStore(editor, {
  deps: [comments],
  project: () =>
    comments.map((comment) => ({
      anchor: comment.anchor,
      data: comment,
      id: comment.id,
      projection: { tone: comment.tone },
    })),
});
 
return (
  <Slate annotationStore={annotationStore} editor={editor}>
    <Editable renderSegment={renderCommentSegment} />
    <CommentsSidebar />
  </Slate>
);

data is app-facing metadata. projection is the small render-facing payload copied into text slices. Keep comment bodies, permissions, resolved state, and audit events in the app or sync service.

Widgets

Widgets describe UI anchored to nodes, selections, or annotations. They are useful for comment popovers, inline toolbars, floating action buttons, and side-panel rows that need resolved visibility.

const widgetStore = useSlateWidgetStore(editor, {
  annotationStore,
  deps: [comments],
  project: () =>
    comments.map((comment) => ({
      anchor: { annotationId: comment.id, type: "annotation" },
      data: { label: comment.label },
      id: `comment-widget:${comment.id}`,
    })),
});
const widgetStore = useSlateWidgetStore(editor, {
  annotationStore,
  deps: [comments],
  project: () =>
    comments.map((comment) => ({
      anchor: { annotationId: comment.id, type: "annotation" },
      data: { label: comment.label },
      id: `comment-widget:${comment.id}`,
    })),
});

Use useSlateWidgets(widgetStore) when a panel renders every widget. Use useSlateWidget(widgetStore, id) when one component watches one widget.

Performance Rules

Projection should reduce render work, not hide it.

RuleWhy
Hoist render callbacksStable renderElement, renderLeaf, renderText, and renderSegment props avoid avoidable text rerenders.
Prefer source-scoped refreshDecoration and annotation sources can refresh the ranges they own instead of rerunning one global decorate callback.
Keep projection payloads smallLarge objects in projection make text-slice comparison and rerender debugging harder.
Prove visible behaviorOverlay correctness needs screenshot or DOM-visible proof when the bug is visual.

Use Improving Performance for render and huge-document guidance.