Excalidraw adds a void excalidraw element that embeds the Excalidraw canvas in the editor. The node stores Excalidraw elements and state 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.elements and app state stored on the node.viewModeEnabled.ExcalidrawKit installs ExcalidrawPlugin with the registry ExcalidrawElement.
'use client';
import type { OrderedExcalidrawElement } from '@excalidraw/excalidraw/element/types';
import type { AppState, ExcalidrawProps } from '@excalidraw/excalidraw/types';
import { ExcalidrawPlugin } from '@platejs/excalidraw/react';
import {
type PlateElementProps,
PlateElement,
useEditor,
useEditorReadOnly,
} from 'platejs/react';
import * as React from 'react';
import { cn } from '@/lib/utils';
import '@excalidraw/excalidraw/index.css';
export function ExcalidrawElement(
props: PlateElementProps<typeof ExcalidrawPlugin>
) {
const { children, element } = props;
const [Excalidraw, setExcalidraw] = React.useState<
(typeof import('@excalidraw/excalidraw'))['Excalidraw'] | null
>(null);
const editor = useEditor();
const readOnly = useEditorReadOnly();
const lastSavedDataRef = React.useRef(
element.data ? JSON.stringify(element.data) : null
);
React.useEffect(() => {
void import('@excalidraw/excalidraw').then((module) => {
setExcalidraw(() => module.Excalidraw);
});
}, []);
// Excalidraw treats initialData as an initialization boundary and mutates it.
const initialData = React.useMemo(
() => ({
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[])
: [],
libraryItems: [],
scrollToContent: true,
}),
[element.data]
);
const excalidrawProps = {
autoFocus: false,
initialData,
onChange: readOnly
? undefined
: (
elements: readonly OrderedExcalidrawElement[],
state: Partial<AppState>
) => {
const dataJson = JSON.stringify({ elements, state });
if (lastSavedDataRef.current === dataJson) return;
const path = editor.read.nodes.path(element);
if (!path) return;
lastSavedDataRef.current = dataJson;
editor.update.nodes.set(
{ data: JSON.parse(dataJson) as NonNullable<typeof element.data> },
{ at: path }
);
},
} satisfies ExcalidrawProps;
return (
<PlateElement {...props}>
<div contentEditable={false}>
<div
className={cn(
'mx-auto aspect-video h-[600px] w-[min(100%,600px)] overflow-hidden rounded-sm border'
)}
>
{Excalidraw && (
<Excalidraw {...excalidrawProps} viewModeEnabled={readOnly} />
)}
</div>
</div>
{children}
</PlateElement>
);
}
export const ExcalidrawKit = [
ExcalidrawPlugin.configure({ component: ExcalidrawElement }),
];'use client';
import type { OrderedExcalidrawElement } from '@excalidraw/excalidraw/element/types';
import type { AppState, ExcalidrawProps } from '@excalidraw/excalidraw/types';
import { ExcalidrawPlugin } from '@platejs/excalidraw/react';
import {
type PlateElementProps,
PlateElement,
useEditor,
useEditorReadOnly,
} from 'platejs/react';
import * as React from 'react';
import { cn } from '@/lib/utils';
import '@excalidraw/excalidraw/index.css';
export function ExcalidrawElement
import { createPlateEditor } from 'platejs/react';
import { ExcalidrawKit } from '@/components/editor/excalidraw';
export const editor = createPlateEditor({
plugins: ExcalidrawKit,
});import { createPlateEditor } from 'platejs/react';
import { ExcalidrawKit } from '@/components/editor/excalidraw';
export const editor = createPlateEditor({
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, ExcalidrawProps } from '@excalidraw/excalidraw/types';
import { ExcalidrawPlugin } from '@platejs/excalidraw/react';
import {
type PlateElementProps,
PlateElement,
useEditor,
useEditorReadOnly,
} from 'platejs/react';
import * as React from 'react';
import { cn } from '@/lib/utils';
import '@excalidraw/excalidraw/index.css';
export function
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 the React ExcalidrawPlugin descriptor. |
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 { createPlateEditor } from 'platejs/react';
import { ExcalidrawElement } from '@/components/editor/excalidraw';
export const editor = createPlateEditor({
plugins: [ExcalidrawPlugin.configure({ component: ExcalidrawElement })],
});import { ExcalidrawPlugin } from '@platejs/excalidraw/react';
import { createPlateEditor } from 'platejs/react';
import { ExcalidrawElement } from '@/components/editor/excalidraw';
export const editor = createPlateEditor({
plugins: [ExcalidrawPlugin.configure({ component: ExcalidrawElement })],
});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.plugin
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 Plite child for the void element. |
data.elements | Excalidraw elements | Stored as partial Excalidraw elements. |
data.state | Excalidraw app state | Stored as imported Excalidraw app state. |
Markdown serialization is not owned by @platejs/excalidraw. Persist the Plate value when you need to keep drawings.
The copied registry element owns the Excalidraw React integration directly.
| Surface | Behavior |
|---|---|
| Component loading | Dynamically imports @excalidraw/excalidraw and returns the loaded component. |
| Initial data | Deep-clones element.data.state, element.data.elements, libraryItems, and scrollToContent. |
| Editing | onChange writes { elements, state } back to the node. |
| Deduplication | Uses deep equality to skip writes when canvas data did not change. |
| Read-only mode | Removes the write handler and enables Excalidraw viewModeEnabled. |
| 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. |
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, ExcalidrawProps } from '@excalidraw/excalidraw/types';
import { ExcalidrawPlugin } from '@platejs/excalidraw/react';
import {
type PlateElementProps,
PlateElement,
useEditor,
useEditorReadOnly,
} from 'platejs/react';
import * as React from 'react';
import { cn } from '@/lib/utils';
import '@excalidraw/excalidraw/index.css';
export function ExcalidrawElement(
props: PlateElementProps<typeof ExcalidrawPlugin>
) {
const { children, element } = props;
const [Excalidraw, setExcalidraw] = React.useState<
(typeof import('@excalidraw/excalidraw'))['Excalidraw'] | null
>(null);
const editor = useEditor();
const readOnly = useEditorReadOnly();
const lastSavedDataRef = React.useRef(
element.data ? JSON.stringify(element.data) : null
);
React.useEffect(() => {
void import('@excalidraw/excalidraw').then((module) => {
setExcalidraw(() => module.Excalidraw);
});
}, []);
// Excalidraw treats initialData as an initialization boundary and mutates it.
const initialData = React.useMemo(
() => ({
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[])
: [],
libraryItems: [],
scrollToContent: true,
}),
[element.data]
);
const excalidrawProps = {
autoFocus: false,
initialData,
onChange: readOnly
? undefined
: (
elements: readonly OrderedExcalidrawElement[],
state: Partial<AppState>
) => {
const dataJson = JSON.stringify({ elements, state });
if (lastSavedDataRef.current === dataJson) return;
const path = editor.read.nodes.path(element);
if (!path) return;
lastSavedDataRef.current = dataJson;
editor.update.nodes.set(
{ data: JSON.parse(dataJson) as NonNullable<typeof element.data> },
{ at: path }
);
},
} satisfies ExcalidrawProps;
return (
<PlateElement {...props}>
<div contentEditable={false}>
<div
className={cn(
'mx-auto aspect-video h-[600px] w-[min(100%,600px)] overflow-hidden rounded-sm border'
)}
>
{Excalidraw && (
<Excalidraw {...excalidrawProps} viewModeEnabled={readOnly} />
)}
</div>
</div>
{children}
</PlateElement>
);
}
export const ExcalidrawKit = [
ExcalidrawPlugin.configure({ component: ExcalidrawElement }),
];