Plate
PlateEditorsTemplates
GitHub16kGitHub
DiscordDiscord
  • Introduction
  • Installation
    • Plate UI
      • Next.js
      • React
    • Manual
    • RSC
    • Node.js
    • Local Docs
    • MCP
  • Releases

Excalidraw

PreviousNext

Void Excalidraw drawing blocks stored inside Plate values.

Excalidraw Element

Excalidraw adds a void excalidraw element that embeds the Excalidraw canvas in the editor. The node stores Excalidraw elements, exported app state, and referenced image files under data. This page covers kit setup, insertion, persistence shape, and the client-only registry UI.

Loading…
EquationFootnote

On This Page

FeaturesFast pathAdd the kitRender the elementAdd an insert actionOwnershipManual setupInstall packageAdd the pluginInsert a drawingValue shapeUI behaviorAPI Reference
Build your editor
Production-ready AI template and reusable components.
Get all-access

Features

  • Void excalidraw block element.
  • Descriptor-scoped editor.plugin(BaseExcalidrawPlugin).update.insert(props, options) command.
  • Drawing elements, persistent app state, and referenced image files stored on the node.
  • Lazy component loading in the copied registry UI.
  • External scene updates and document undo reflected in the canvas.
  • Transient viewport and selection changes excluded from document saves.
  • Read-only mode through Excalidraw viewModeEnabled.
Report an issue

Fast path

Add the kit

ExcalidrawKit installs ExcalidrawPlugin with the registry ExcalidrawElement.

'use client';
 
import type { OrderedExcalidrawElement } from '@excalidraw/excalidraw/element/types';
import type {
  AppState,
  BinaryFiles,
  ExcalidrawImperativeAPI,
} from '@excalidraw/excalidraw/types';
import { ExcalidrawPlugin, useExcalidrawSync } from 'platejs/excalidraw/react';
import {
  type EditorElementProps,
  EditorElement,
  useEditorReadOnly,
} from 'platejs/react';
import * as React from 'react';
 
import { cn } from '@/lib/utils';
 
import '@excalidraw/excalidraw/index.css';
 
export function ExcalidrawElement(
  props: EditorElementProps<typeof ExcalidrawPlugin>
) {
  const { children, element } = props;
  const [excalidraw, setExcalidraw] = React.useState<
    typeof import('@excalidraw/excalidraw') | null
  >(null);
  const [api, setApi] = React.useState<ExcalidrawImperativeAPI | null>(null);
  const readOnly = useEditorReadOnly();
  const Excalidraw = excalidraw?.Excalidraw;
  useExcalidrawSync({ api, excalidraw });
 
  React.useEffect(() => {
    let active = true;
    void import('@excalidraw/excalidraw').then((module) => {
      if (active) setExcalidraw(module);
    });
    return () => {
      active = false;
    };
  }, []);
 
  // Excalidraw treats initialData as an initialization boundary and mutates it.
  const [initialData] = React.useState(() => ({
    appState: element.data?.state
      ? (structuredClone(element.data.state) as Partial<AppState>)
      : undefined,
    elements: element.data?.elements
      ? (structuredClone(
          element.data.elements
        ) as unknown as readonly OrderedExcalidrawElement[])
      : [],
    files: element.data?.files
      ? (structuredClone(element.data.files) as unknown as BinaryFiles)
      : undefined,
    libraryItems: [],
    scrollToContent: true,
  }));
 
  return (
    <EditorElement {...props}>
      <div contentEditable={false} data-editor-root-chrome-ignore="true">
        <div
          className={cn(
            'mx-auto aspect-video h-[600px] w-[min(100%,600px)] overflow-hidden rounded-sm border'
          )}
        >
          {Excalidraw && (
            <Excalidraw
              autoFocus={false}
              excalidrawAPI={setApi}
              initialData={initialData}
              viewModeEnabled={readOnly}
            />
          )}
        </div>
      </div>
      {children}
    </EditorElement>
  );
}
 
export const ExcalidrawKit = [
  ExcalidrawPlugin.configure({ component: ExcalidrawElement }),
];
'use client';
 
import type { OrderedExcalidrawElement } from '@excalidraw/excalidraw/element/types';
import type {
  AppState,
  BinaryFiles,
  ExcalidrawImperativeAPI,
} from '@excalidraw/excalidraw/types';
import { ExcalidrawPlugin, useExcalidrawSync } from 'platejs/excalidraw/react';
import {
  type EditorElementProps,
  EditorElement,
  useEditorReadOnly,
} from 'platejs/react';
import * as React from 'react';
 
import { cn } from '@/lib/utils';
 
import '@excalidraw/excalidraw/index.css'

































































import { createEditor } from 'platejs/react';
 
import { ExcalidrawKit } from '@/components/editor/excalidraw';
 
export const editor = createEditor({
  plugins: ExcalidrawKit,
});
import { createEditor } from 'platejs/react';
 
import { ExcalidrawKit } from '@/components/editor/excalidraw';
 
export const editor = createEditor({
  plugins: ExcalidrawKit,

Render the element

excalidraw owns the client component, Excalidraw CSS import, fixed canvas frame, and read-only view mode.

'use client';
 
import type { OrderedExcalidrawElement } from '@excalidraw/excalidraw/element/types';
import type {
  AppState,
  BinaryFiles,
  ExcalidrawImperativeAPI,
} from '@excalidraw/excalidraw/types';
import { ExcalidrawPlugin, useExcalidrawSync } from 'platejs/excalidraw/react';
import {
  type EditorElementProps,
  EditorElement,
  useEditorReadOnly,
} from 'platejs/react';
import * as React from 'react';
 
import { cn } from '@/lib/utils';
 
import

































































Add an insert action

The registry insert toolbar calls the installed Excalidraw command.

components/editor/transforms.ts
import { BaseExcalidrawPlugin } from 'platejs/excalidraw';
import { PLUGINS } from 'platejs';
 
export const insertBlockMap = {
  [PLUGINS.excalidraw]: (editor) =>
    editor.plugin(BaseExcalidrawPlugin).update.insert({}, { select: true }),
};
components/editor/transforms.ts
import { BaseExcalidrawPlugin } from 'platejs/excalidraw'





Ownership

LayerOwnerWhat It Does
platejs/excalidrawPackageExports BaseExcalidrawPlugin, ExcalidrawElement, and ExcalidrawDataState.
platejs/excalidraw/reactPackageExports ExcalidrawPlugin and the useExcalidrawSync lifecycle hook.
excalidrawRegistryAdds ExcalidrawPlugin.configure({ component: ExcalidrawElement }).
excalidrawRegistry UIDynamically renders @excalidraw/excalidraw inside a Plate element.
App persistenceApp codeStores the Plate value that contains Excalidraw element data.

BaseExcalidrawPlugin owns the standard descriptor-scoped insert update.

Manual setup

Install package

pnpm add platejs @excalidraw/excalidraw @tanstack/react-virtual
pnpm add platejs @excalidraw/excalidraw @tanstack/react-virtual

Add the plugin

Use the React plugin when the editor renders the Excalidraw canvas.

import { ExcalidrawPlugin } from 'platejs/excalidraw/react';
import { createEditor } from 'platejs/react';
 
import { ExcalidrawElement } from '@/components/editor/excalidraw';
 
export const editor = createEditor({
  plugins: [ExcalidrawPlugin.configure({ component: ExcalidrawElement })],
});
import { ExcalidrawPlugin } from 'platejs/excalidraw/react';
import { createEditor } from 'platejs/react'





Insert a drawing

The plugin portal's insert method is the standard descriptor-bound block insertion. Pass at for an exact location; otherwise it inserts after the selected block. It is a no-op without a selection or explicit at.

import { BaseExcalidrawPlugin } from 'platejs/excalidraw';
 
editor.plugin(BaseExcalidrawPlugin).update.insert(
  {
    data: {
      elements: [],
      state: {
        viewBackgroundColor: '#ffffff',
      },
    },
  },
  { select: true }
);
import { BaseExcalidrawPlugin } from 'platejs/excalidraw';
 
editor.









Value shape

ExcalidrawElement is a void element. The drawing payload lives in data, not in text children.

const value = [
  {
    children: [{ text: '' }],
    data: {
      elements: [
        {
          id: 'shape-1',
          type: 'rectangle',
          x: 100,
          y: 100,
        },
      ],
      state: {
        viewBackgroundColor: '#ffffff',
      },
    },
    type: 'excalidraw',
  },
];
const value = [
  {
    children: [{ text: '' }],
    data: {
      elements: [
        {
          id: 'shape-1',
          type: 'rectangle',
          x: 100,
          y: 100,
        },
      ],
      state: {
        viewBackgroundColor: '#ffffff',
      },
    },
    type: 'excalidraw',
  },
];
FieldTypeNotes
type'excalidraw'Persisted element type owned by BaseExcalidrawPlugin.
children[{ text: '' }]Required Plate child for the void element.
data.elementsExcalidraw elementsStored as partial Excalidraw elements.
data.stateExported Excalidraw app statePersistent drawing settings; excludes viewport, selection, and open menus.
data.filesOptional Excalidraw binary-file mapEmbedded image assets referenced by the drawing.

Markdown serialization is not owned by platejs/excalidraw. Persist the Plate value when you need to keep drawings.

UI behavior

The copied registry element owns loading, initial framing, controls, and layout. useExcalidrawSync({ api, excalidraw }) owns the mounted canvas's document synchronization. Pass the imperative API and the lazily loaded Excalidraw namespace; both may be null before loading finishes. The hook reads the current Excalidraw element context and returns void.

SurfaceBehavior
Component loadingDynamically imports @excalidraw/excalidraw; ignores completion after unmount.
Initial dataCreates an isolated initial scene copy for Excalidraw's mutable initialization boundary.
EditingUses Excalidraw's local export serializer to save drawing elements, persistent state, and referenced files.
DeduplicationSkips unchanged serialized drawings, including pan, zoom, and selection-only events.
External data and document undoRestores the current node into the canvas without a save echo and clears incompatible canvas history.
Canvas undoExcalidraw owns local drawing undo; resulting scene changes are saved to the node.
Read-only modeEnables Excalidraw viewModeEnabled and rejects writes at the editor boundary.
Canvas frameRegistry UI renders a bordered aspect-video frame capped at 600px.

The registry element imports @excalidraw/excalidraw/index.css, so custom copies need the same stylesheet.

API Reference

APIPackageUse
BaseExcalidrawPluginplatejs/excalidrawHeadless void element plugin.
ExcalidrawPluginplatejs/excalidraw/reactReact Excalidraw plugin.
useExcalidrawSync({ api, excalidraw })platejs/excalidraw/reactSynchronizes a mounted canvas with its current element.
editor.plugin(BaseExcalidrawPlugin).update.insert(props?, options?)BaseExcalidrawPluginInserts a void Excalidraw node at options.at or after the selected block.
ExcalidrawElementplatejs/excalidrawElement shape with optional data.
ExcalidrawDataStateplatejs/excalidrawData shape for stored Excalidraw elements and app state.
;
export function ExcalidrawElement(
props: EditorElementProps<typeof ExcalidrawPlugin>
) {
const { children, element } = props;
const [excalidraw, setExcalidraw] = React.useState<
typeof import('@excalidraw/excalidraw') | null
>(null);
const [api, setApi] = React.useState<ExcalidrawImperativeAPI | null>(null);
const readOnly = useEditorReadOnly();
const Excalidraw = excalidraw?.Excalidraw;
useExcalidrawSync({ api, excalidraw });
React.useEffect(() => {
let active = true;
void import('@excalidraw/excalidraw').then((module) => {
if (active) setExcalidraw(module);
});
return () => {
active = false;
};
}, []);
// Excalidraw treats initialData as an initialization boundary and mutates it.
const [initialData] = React.useState(() => ({
appState: element.data?.state
? (structuredClone(element.data.state) as Partial<AppState>)
: undefined,
elements: element.data?.elements
? (structuredClone(
element.data.elements
) as unknown as readonly OrderedExcalidrawElement[])
: [],
files: element.data?.files
? (structuredClone(element.data.files) as unknown as BinaryFiles)
: undefined,
libraryItems: [],
scrollToContent: true,
}));
return (
<EditorElement {...props}>
<div contentEditable={false} data-editor-root-chrome-ignore="true">
<div
className={cn(
'mx-auto aspect-video h-[600px] w-[min(100%,600px)] overflow-hidden rounded-sm border'
)}
>
{Excalidraw && (
<Excalidraw
autoFocus={false}
excalidrawAPI={setApi}
initialData={initialData}
viewModeEnabled={readOnly}
/>
)}
</div>
</div>
{children}
</EditorElement>
);
}
export const ExcalidrawKit = [
ExcalidrawPlugin.configure({ component: ExcalidrawElement }),
];
});
'@excalidraw/excalidraw/index.css'
;
export function ExcalidrawElement(
props: EditorElementProps<typeof ExcalidrawPlugin>
) {
const { children, element } = props;
const [excalidraw, setExcalidraw] = React.useState<
typeof import('@excalidraw/excalidraw') | null
>(null);
const [api, setApi] = React.useState<ExcalidrawImperativeAPI | null>(null);
const readOnly = useEditorReadOnly();
const Excalidraw = excalidraw?.Excalidraw;
useExcalidrawSync({ api, excalidraw });
React.useEffect(() => {
let active = true;
void import('@excalidraw/excalidraw').then((module) => {
if (active) setExcalidraw(module);
});
return () => {
active = false;
};
}, []);
// Excalidraw treats initialData as an initialization boundary and mutates it.
const [initialData] = React.useState(() => ({
appState: element.data?.state
? (structuredClone(element.data.state) as Partial<AppState>)
: undefined,
elements: element.data?.elements
? (structuredClone(
element.data.elements
) as unknown as readonly OrderedExcalidrawElement[])
: [],
files: element.data?.files
? (structuredClone(element.data.files) as unknown as BinaryFiles)
: undefined,
libraryItems: [],
scrollToContent: true,
}));
return (
<EditorElement {...props}>
<div contentEditable={false} data-editor-root-chrome-ignore="true">
<div
className={cn(
'mx-auto aspect-video h-[600px] w-[min(100%,600px)] overflow-hidden rounded-sm border'
)}
>
{Excalidraw && (
<Excalidraw
autoFocus={false}
excalidrawAPI={setApi}
initialData={initialData}
viewModeEnabled={readOnly}
/>
)}
</div>
</div>
{children}
</EditorElement>
);
}
export const ExcalidrawKit = [
ExcalidrawPlugin.configure({ component: ExcalidrawElement }),
];
'use client';
 
import type { OrderedExcalidrawElement } from '@excalidraw/excalidraw/element/types';
import type {
  AppState,
  BinaryFiles,
  ExcalidrawImperativeAPI,
} from '@excalidraw/excalidraw/types';
import { ExcalidrawPlugin, useExcalidrawSync } from 'platejs/excalidraw/react';
import {
  type EditorElementProps,
  EditorElement,
  useEditorReadOnly,
} from 'platejs/react';
import * as React from 'react';
 
import { cn } from '@/lib/utils';
 
import '@excalidraw/excalidraw/index.css';
 
export function ExcalidrawElement(
  props: EditorElementProps<typeof ExcalidrawPlugin>
) {
  const { children, element } = props;
  const [excalidraw, setExcalidraw] = React.useState<
    typeof import('@excalidraw/excalidraw') | null
  >(null);
  const [api, setApi] = React.useState<ExcalidrawImperativeAPI | null>(null);
  const readOnly = useEditorReadOnly();
  const Excalidraw = excalidraw?.Excalidraw;
  useExcalidrawSync({ api, excalidraw });
 
  React.useEffect(() => {
    let active = true;
    void import('@excalidraw/excalidraw').then((module) => {
      if (active) setExcalidraw(module);
    });
    return () => {
      active = false;
    };
  }, []);
 
  // Excalidraw treats initialData as an initialization boundary and mutates it.
  const [initialData] = React.useState(() => ({
    appState: element.data?.state
      ? (structuredClone(element.data.state) as Partial<AppState>)
      : undefined,
    elements: element.data?.elements
      ? (structuredClone(
          element.data.elements
        ) as unknown as readonly OrderedExcalidrawElement[])
      : [],
    files: element.data?.files
      ? (structuredClone(element.data.files) as unknown as BinaryFiles)
      : undefined,
    libraryItems: [],
    scrollToContent: true,
  }));
 
  return (
    <EditorElement {...props}>
      <div contentEditable={false} data-editor-root-chrome-ignore="true">
        <div
          className={cn(
            'mx-auto aspect-video h-[600px] w-[min(100%,600px)] overflow-hidden rounded-sm border'
          )}
        >
          {Excalidraw && (
            <Excalidraw
              autoFocus={false}
              excalidrawAPI={setApi}
              initialData={initialData}
              viewModeEnabled={readOnly}
            />
          )}
        </div>
      </div>
      {children}
    </EditorElement>
  );
}
 
export const ExcalidrawKit = [
  ExcalidrawPlugin.configure({ component: ExcalidrawElement }),
];
;
import { PLUGINS } from 'platejs';
export const insertBlockMap = {
[PLUGINS.excalidraw]: (editor) =>
editor.plugin(BaseExcalidrawPlugin).update.insert({}, { select: true }),
};
;
import { ExcalidrawElement } from '@/components/editor/excalidraw';
export const editor = createEditor({
plugins: [ExcalidrawPlugin.configure({ component: ExcalidrawElement })],
});
plugin
(BaseExcalidrawPlugin).update.
insert
(
{
data: {
elements: [],
state: {
viewBackgroundColor: '#ffffff',
},
},
},
{ select: true }
);