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 and ExcalidrawElement. |
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 object keyed by file id | Embedded image assets referenced by the drawing. Any other value fails validation. |
Type a stored drawing as NonNullable<ExcalidrawElement['data']>. Excalidraw's own input, such as initialData, uses Excalidraw's ImportedDataState type, which names the settings appState instead of state.
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. |
| Undo and Redo shortcuts | Run the document's history, also while the canvas has focus; the hook redraws the result into the canvas. They do nothing in read-only mode. |
| Canvas undo buttons | Undo Excalidraw's local steps; the hook saves the resulting scene 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. |
A custom canvas component keeps three parts of the registry element:
contentEditable={false} wrapper with data-editor-root-chrome-ignore="true", so the canvas handles its own input. See Embedded widgets.viewModeEnabled={readOnly}, with readOnly from useEditorReadOnly(), so a read-only document stays in Excalidraw's view mode.@excalidraw/excalidraw/index.css import.| 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; NonNullable<ExcalidrawElement['data']> types a stored drawing. |
'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 }),
];