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.
excalidraw block element.editor.plugin(BaseExcalidrawPlugin).update.insert(props, options) command.viewModeEnabled.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,
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
The registry insert toolbar calls the installed Excalidraw command.
import { BaseExcalidrawPlugin } from 'platejs/excalidraw';
import { PLUGINS } from 'platejs';
export const insertBlockMap = {
[PLUGINS.excalidraw]: (editor) =>
editor.plugin(BaseExcalidrawPlugin).update.insert({}, { select: true }),
};import { BaseExcalidrawPlugin } from 'platejs/excalidraw'
| Layer | Owner | What It Does |
|---|---|---|
platejs/excalidraw | Package | Exports BaseExcalidrawPlugin, ExcalidrawElement, and ExcalidrawDataState. |
platejs/excalidraw/react | Package | Exports ExcalidrawPlugin and the useExcalidrawSync lifecycle hook. |
excalidraw | Registry | Adds ExcalidrawPlugin.configure({ component: ExcalidrawElement }). |
excalidraw | Registry UI | Dynamically renders @excalidraw/excalidraw inside a Plate element. |
| App persistence | App code | Stores the Plate value that contains Excalidraw element data. |
BaseExcalidrawPlugin owns the standard descriptor-scoped insert update.
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'
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.
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',
},
];| Field | Type | Notes |
|---|---|---|
type | 'excalidraw' | Persisted element type owned by BaseExcalidrawPlugin. |
children | [{ text: '' }] | Required Plate child for the void element. |
data.elements | Excalidraw elements | Stored as partial Excalidraw elements. |
data.state | Exported Excalidraw app state | Persistent drawing settings; excludes viewport, selection, and open menus. |
data.files | Optional Excalidraw binary-file map | Embedded image assets referenced by the drawing. |
Markdown serialization is not owned by platejs/excalidraw. Persist the Plate value when you need to keep drawings.
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.
| Surface | Behavior |
|---|---|
| Component loading | Dynamically imports @excalidraw/excalidraw; ignores completion after unmount. |
| Initial data | Creates an isolated initial scene copy for Excalidraw's mutable initialization boundary. |
| Editing | Uses Excalidraw's local export serializer to save drawing elements, persistent state, and referenced files. |
| Deduplication | Skips unchanged serialized drawings, including pan, zoom, and selection-only events. |
| External data and document undo | Restores the current node into the canvas without a save echo and clears incompatible canvas history. |
| Canvas undo | Excalidraw owns local drawing undo; resulting scene changes are saved to the node. |
| Read-only mode | Enables Excalidraw viewModeEnabled and rejects writes at the editor boundary. |
| Canvas frame | Registry 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 | Package | Use |
|---|---|---|
BaseExcalidrawPlugin | platejs/excalidraw | Headless void element plugin. |
ExcalidrawPlugin | platejs/excalidraw/react | React Excalidraw plugin. |
useExcalidrawSync({ api, excalidraw }) | platejs/excalidraw/react | Synchronizes a mounted canvas with its current element. |
editor.plugin(BaseExcalidrawPlugin).update.insert(props?, options?) | BaseExcalidrawPlugin | Inserts a void Excalidraw node at options.at or after the selected block. |
ExcalidrawElement | platejs/excalidraw | Element shape with optional data. |
ExcalidrawDataState | platejs/excalidraw | Data shape for stored Excalidraw elements and app state. |
'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 }),
];