The fastest way to add code drawing functionality is with the CodeDrawingKit, which includes the pre-configured CodeDrawingPlugin with its Plate UI components.
'use client';
import { DownloadIcon, Trash2 } from 'lucide-react';
import {
type CodeDrawingLanguage,
type CodeDrawingView,
CODE_DRAWING_LANGUAGES,
CODE_DRAWING_VIEWS,
renderCodeDrawing,
} from 'platejs/code-drawing';
import { CodeDrawingPlugin } from 'platejs/code-drawing/react';
import {
type EditorElementProps,
EditorElement,
useEditor,
useEditorReadOnly,
useEditorSelector,
useElement,
useElementSelected,
useFocusedLast,
} from 'platejs/react';
import * as React from 'react';
import { Button } from '@/components/ui/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
FloatingPopover,
FloatingPopoverAnchor,
FloatingPopoverContent,
} from '@/components/editor/floating-popover';
import { useIsMobile } from '@/hooks/use-mobile';
const DEFAULT_MIN_HEIGHT = 300;
const languageLabels: Record<CodeDrawingLanguage, string> = {
flowchart: 'Flowchart',
graphviz: 'Graphviz',
mermaid: 'Mermaid',
plantuml: 'PlantUML',
};
const viewLabels: Record<CodeDrawingView, string> = {
code: 'Code',
preview: 'Preview',
split: 'Split',
};
const isCodeDrawingLanguage = (
value: string | null
): value is CodeDrawingLanguage =>
CODE_DRAWING_LANGUAGES.some((language) => language === value);
const isCodeDrawingView = (value: string | null): value is CodeDrawingView =>
CODE_DRAWING_VIEWS.some((view) => view === value);
export function CodeDrawingElement({
plantUmlServer = 'https://www.plantuml.com/plantuml',
...props
}: EditorElementProps<typeof CodeDrawingPlugin> & { plantUmlServer?: string }) {
const { children } = props;
const isMobile = useIsMobile();
const editor = useEditor();
const readOnly = useEditorReadOnly();
const selected = useElementSelected();
const isFocusedLast = useFocusedLast();
const element = useElement(CodeDrawingPlugin);
const { code, language, view } = element;
const selectionCollapsed = useEditorSelector((innerEditor) =>
innerEditor.read.selection.isCollapsed()
);
const open = isFocusedLast && !readOnly && selected && selectionCollapsed;
const demanded = view !== 'code' || open;
const wasDemanded = React.useRef(false);
const [result, setResult] = React.useState<{
code: string;
language: CodeDrawingLanguage;
plantUmlServer: string;
image: string;
error: string | null;
} | null>(null);
const currentResult =
result?.code === code &&
result.language === language &&
result.plantUmlServer === plantUmlServer
? result
: null;
const image = currentResult?.image ?? '';
const renderError = currentResult?.error ?? null;
const loading = demanded && !!code.trim() && !currentResult;
React.useEffect(() => {
const activated = demanded && !wasDemanded.current;
wasDemanded.current = demanded;
if (!demanded || !code.trim() || currentResult) return undefined;
let cancelled = false;
const render = async () => {
try {
const imageData = await renderCodeDrawing(language, code, {
plantUmlServer,
});
if (!cancelled) {
setResult({
code,
language,
plantUmlServer,
image: imageData,
error: null,
});
}
} catch (error) {
if (!cancelled) {
const message =
error instanceof Error ? error.message : 'Rendering failed';
console.error(message);
setResult({
code,
language,
plantUmlServer,
image: '',
error: message,
});
}
}
};
const timeout = window.setTimeout(
() => {
void render();
},
activated ? 0 : 500
);
return () => {
cancelled = true;
window.clearTimeout(timeout);
};
}, [code, currentResult, demanded, language, plantUmlServer]);
const handleCodeChange = React.useCallback(
(nextCode: string) => {
const path = editor.read.nodes.path(element);
if (path) {
editor.update.nodes.set({ code: nextCode }, { at: path });
}
},
[editor, element]
);
const handleLanguageChange = React.useCallback(
(nextLanguage: CodeDrawingLanguage) => {
const path = editor.read.nodes.path(element);
if (path) {
editor.update.nodes.set({ language: nextLanguage }, { at: path });
}
},
[editor, element]
);
const handleViewChange = React.useCallback(
(nextView: CodeDrawingView) => {
const path = editor.read.nodes.path(element);
if (path) {
editor.update.nodes.set({ view: nextView }, { at: path });
}
},
[editor, element]
);
const content = (
<EditorElement {...props}>
<div contentEditable={false}>
<div>
<CodeDrawingPreview
code={code}
language={language}
view={view}
image={image}
loading={loading}
renderError={renderError}
onCodeChange={handleCodeChange}
onLanguageChange={handleLanguageChange}
onViewChange={handleViewChange}
readOnly={readOnly}
isMobile={isMobile}
/>
</div>
</div>
{children}
</EditorElement>
);
if (readOnly) {
return content;
}
return (
<FloatingPopover open={open} modal={false}>
<FloatingPopoverAnchor element={content} />
<FloatingPopoverContent
className="w-auto p-1"
contentEditable={false}
onInitialFocus={(e) => {
e.preventDefault();
}}
>
<div className="flex items-center gap-1">
{image && (
<Button
size="icon"
variant="ghost"
className="size-8"
onClick={(event) => {
const { ownerDocument } = event.currentTarget;
const imageElement = ownerDocument.createElement('img');
imageElement.addEventListener(
'load',
() => {
const canvas = ownerDocument.createElement('canvas');
canvas.width = imageElement.naturalWidth;
canvas.height = imageElement.naturalHeight;
const context = canvas.getContext('2d');
if (!context) return;
context.drawImage(imageElement, 0, 0);
const link = ownerDocument.createElement('a');
link.href = canvas.toDataURL('image/png');
link.download = 'code-drawing.png';
link.click();
},
{ once: true }
);
imageElement.src = image;
}}
title="Export"
>
<DownloadIcon className="size-4" />
</Button>
)}
<Button
size="icon"
variant="ghost"
className="size-8"
onClick={() => {
const path = editor.read.nodes.path(element);
if (!readOnly && path) editor.update.nodes.remove({ at: path });
}}
title="Delete"
>
<Trash2 className="size-4" />
</Button>
</div>
</FloatingPopoverContent>
</FloatingPopover>
);
}
function CodeDrawingPreview({
code,
language,
view,
image,
loading,
renderError,
onCodeChange,
onLanguageChange,
onViewChange,
readOnly = false,
isMobile = false,
}: {
code: string;
language: CodeDrawingLanguage;
view: CodeDrawingView;
image: string;
loading: boolean;
renderError: string | null;
onCodeChange: (code: string) => void;
onLanguageChange: (language: CodeDrawingLanguage) => void;
onViewChange: (view: CodeDrawingView) => void;
readOnly?: boolean;
isMobile?: boolean;
}) {
const viewMode = view;
const showCode = viewMode === 'split' || viewMode === 'code';
const showBorder = viewMode === 'split';
const handleCodeChange = React.useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
onCodeChange(e.target.value);
},
[onCodeChange]
);
const toolbar = readOnly ? null : (
<CodeDrawingToolbar
language={language}
viewMode={viewMode}
readOnly={readOnly}
isMobile={isMobile}
onLanguageChange={onLanguageChange}
onViewChange={onViewChange}
/>
);
return (
<div
className={`flex ${
isMobile ? 'flex-col-reverse' : 'flex-col'
} my-4 w-full items-stretch border bg-muted/50 md:flex-row hover:[&_[role=toolbar]]:opacity-100`}
style={{
minHeight: `${DEFAULT_MIN_HEIGHT}px`,
}}
>
{showCode && (
<CodeDrawingTextarea
code={code}
viewMode={viewMode}
readOnly={readOnly}
isMobile={isMobile}
showBorder={showBorder}
onCodeChange={handleCodeChange}
toolbar={viewMode === 'code' ? toolbar : null}
/>
)}
{viewMode !== 'code' && (
<CodeDrawingPreviewArea
image={image}
loading={loading}
renderError={renderError}
code={code}
viewMode={viewMode}
readOnly={readOnly}
isMobile={isMobile}
showBorder={showBorder}
toolbar={toolbar}
/>
)}
</div>
);
}
function CodeDrawingToolbar({
language,
viewMode,
readOnly = false,
isMobile = false,
onLanguageChange,
onViewChange,
}: {
language: CodeDrawingLanguage;
viewMode: CodeDrawingView;
readOnly?: boolean;
isMobile?: boolean;
onLanguageChange: (language: CodeDrawingLanguage) => void;
onViewChange: (view: CodeDrawingView) => void;
}) {
const [toolbarVisible, setToolbarVisible] = React.useState(false);
const [languageSelectOpen, setLanguageSelectOpen] = React.useState(false);
const [viewSelectOpen, setViewSelectOpen] = React.useState(false);
const opacityClass =
isMobile || toolbarVisible || languageSelectOpen || viewSelectOpen
? 'opacity-100'
: 'opacity-0';
const positionClass = isMobile
? 'flex items-center gap-2'
: 'absolute right-2 z-10 flex items-center gap-2';
return (
<div
role="toolbar"
tabIndex={-1}
className={`${positionClass} transition-opacity ${opacityClass}`}
onMouseEnter={() => {
setToolbarVisible(true);
}}
onMouseLeave={() => {
if (!languageSelectOpen && !viewSelectOpen) {
setToolbarVisible(false);
}
}}
>
{!readOnly && (
<Select
value={language}
onValueChange={(nextLanguage) => {
if (isCodeDrawingLanguage(nextLanguage)) {
onLanguageChange(nextLanguage);
}
}}
open={languageSelectOpen}
onOpenChange={setLanguageSelectOpen}
>
<SelectTrigger
className={`h-8 w-[120px] border-0 bg-muted/50 text-xs shadow-none ${
isMobile ? '' : 'transition-colors hover:bg-zinc-200'
}`}
>
<SelectValue />
</SelectTrigger>
<SelectContent className="z-[100]">
{CODE_DRAWING_LANGUAGES.map((innerLanguage) => (
<SelectItem key={innerLanguage} value={innerLanguage}>
{languageLabels[innerLanguage]}
</SelectItem>
))}
</SelectContent>
</Select>
)}
{!readOnly && (
<Select
value={viewMode}
onValueChange={(nextView) => {
if (isCodeDrawingView(nextView)) {
onViewChange(nextView);
}
}}
open={viewSelectOpen}
onOpenChange={setViewSelectOpen}
>
<SelectTrigger
className={`h-8 w-[80px] border-0 bg-muted/50 text-xs shadow-none ${
isMobile ? '' : 'transition-colors hover:bg-zinc-200'
}`}
>
<SelectValue />
</SelectTrigger>
<SelectContent className="z-[100]">
{CODE_DRAWING_VIEWS.map((view) => (
<SelectItem key={view} value={view}>
{viewLabels[view]}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
);
}
function CodeDrawingTextarea({
code,
viewMode,
readOnly = false,
isMobile = false,
showBorder = false,
onCodeChange,
toolbar,
}: {
code: string;
viewMode: CodeDrawingView;
readOnly?: boolean;
isMobile?: boolean;
showBorder?: boolean;
onCodeChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
toolbar?: React.ReactNode;
}) {
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
const isCodeOnlyMode = viewMode === 'code';
const [internalCode, setInternalCode] = React.useState(code);
const lastExternalCodeRef = React.useRef(code);
React.useEffect(() => {
if (code !== lastExternalCodeRef.current) {
lastExternalCodeRef.current = code;
setInternalCode(code);
}
}, [code]);
const handleChange = React.useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newValue = e.target.value;
setInternalCode(newValue);
onCodeChange(e);
},
[onCodeChange]
);
return (
<div
className={`${
isCodeOnlyMode ? 'w-full' : 'min-w-0 flex-1'
} flex flex-col ${isCodeOnlyMode && !isMobile ? 'relative' : ''} ${
showBorder && !isMobile ? 'border-r' : ''
}`}
>
{toolbar && isCodeOnlyMode && (
<div
className={
isMobile
? 'mt-2 mb-2 flex justify-end px-2'
: 'absolute right-2 z-10 mt-2'
}
>
{toolbar}
</div>
)}
<div className="relative flex-1 rounded-md">
<pre
className="m-0 overflow-x-auto p-8 pr-4 font-mono text-sm leading-[normal] [tab-size:2] print:break-inside-avoid"
style={{ minHeight: `${DEFAULT_MIN_HEIGHT}px`, height: '100%' }}
>
<code className="block h-full w-full">
<textarea
ref={textareaRef}
value={internalCode}
onChange={handleChange}
readOnly={readOnly}
className="m-0 h-full w-full resize-none overflow-auto border-0 bg-transparent p-0 font-mono text-sm outline-none"
style={{ minHeight: `${DEFAULT_MIN_HEIGHT}px` }}
placeholder="Enter your code here..."
spellCheck={false}
/>
</code>
</pre>
</div>
</div>
);
}
function CodeDrawingPreviewArea({
image,
loading,
renderError,
code,
viewMode,
readOnly: _readOnly = false,
isMobile = false,
showBorder = false,
toolbar,
}: {
image: string;
loading: boolean;
renderError: string | null;
code: string;
viewMode: CodeDrawingView;
readOnly?: boolean;
isMobile?: boolean;
showBorder?: boolean;
toolbar?: React.ReactNode;
}) {
const showPreview = viewMode === 'split' || viewMode === 'preview';
return (
<div
className={`flex min-w-0 flex-1 flex-col ${isMobile ? '' : 'relative'} ${
showBorder && isMobile ? 'border-b' : ''
}`}
>
{toolbar && (
<div
className={
isMobile
? 'mt-2 mb-2 flex justify-end px-2'
: 'absolute right-2 z-10 mt-2'
}
>
{toolbar}
</div>
)}
{showPreview ? (
<div className="flex flex-1 items-center justify-center rounded-md bg-muted/30 p-4">
{loading && <div className="text-muted-foreground">Loading...</div>}
{!loading &&
image && (
// oxlint-disable-next-line nextjs/no-img-element -- [P1 local-invariant] The renderer returns an ephemeral preview data URL; optimization and remote loading do not apply.
<img
src={image}
alt="code drawing"
className="max-h-full max-w-full object-contain"
/>
)}
{!loading && !image && renderError && (
<div className="text-destructive" title={renderError}>
Could not render preview. Edit the source to retry.
</div>
)}
{!loading && !image && !renderError && (
<div className="text-muted-foreground">
{code.trim() ? 'Rendering...' : 'Preview will appear here'}
</div>
)}
</div>
) : (
<div className="pointer-events-none flex flex-1 items-center justify-center rounded-md border bg-muted/30 p-4 opacity-0">
{/* Placeholder to maintain height */}
</div>
)}
</div>
);
}
export const CodeDrawingKit = [
CodeDrawingPlugin.configure({ component: CodeDrawingElement }),
];'use client';
import { DownloadIcon, Trash2 } from 'lucide-react';
import {
type CodeDrawingLanguage,
type CodeDrawingView,
CODE_DRAWING_LANGUAGES,
CODE_DRAWING_VIEWS,
renderCodeDrawing,
} from 'platejs/code-drawing';
import { CodeDrawingPlugin } from 'platejs/code-drawing/react';
import {
type EditorElementProps,
EditorElement,
useEditor,
useEditorReadOnly,
useEditorSelector,
useElement,
useElementSelected,
useFocusedLast,
} from 'platejs/react';
import * as
CodeDrawingElement: Renders code drawing elements with inline editing and preview.Add the kit to your plugins:
import { createEditor } from 'platejs/react';
import { CodeDrawingKit } from '@/components/editor/code-drawing';
const editor = createEditor({
plugins: [
// ...otherPlugins,
...CodeDrawingKit,
],
});import { createEditor } from 'platejs/react';
import { CodeDrawingKit } from '@/components/editor/code-drawing';
const editor =
Include CodeDrawingPlugin in your Plate plugins array when creating the editor.
import { CodeDrawingPlugin } from 'platejs/code-drawing/react';
import { createEditor } from 'platejs/react';
const editor = createEditor({
plugins: [
// ...otherPlugins,
CodeDrawingPlugin,
],
});import { CodeDrawingPlugin } from 'platejs/code-drawing/react';
import { createEditor } from 'platejs/react';
const editor = createEditor
Configure the code drawing plugin with custom components.
import { CodeDrawingPlugin } from 'platejs/code-drawing/react';
import { createEditor } from 'platejs/react';
import { CodeDrawingElement } from '@/components/editor/code-drawing';
const editor = createEditor({
plugins: [
// ...otherPlugins,
CodeDrawingPlugin.configure({ component: CodeDrawingElement }),
],
});import { CodeDrawingPlugin } from 'platejs/code-drawing/react';
component: Assigns CodeDrawingElement to render code drawing elements.Add an item that calls the installed code-drawing feature. Code Drawing is an insertion action; it is not a Turn Into conversion.
import { BaseCodeDrawingPlugin } from 'platejs/code-drawing';
const insertItem = {
icon: <Code2Icon />,
label: 'Code Drawing',
value: 'code_drawing',
onSelect: () => {
const codeDrawing = editor.plugin(BaseCodeDrawingPlugin);
if (!codeDrawing.installed || editor.read.view.isReadOnly()) return;
codeDrawing.update.insert({}, { select: true });
},
};Plugin for rendering code drawings with inline editing and preview capabilities.
Inserts a code drawing element at the current selection.
The code drawing element type.
The serialized element type. Its default is codeDrawing; read the installed
value from editor.plugin(BaseCodeDrawingPlugin).schema.type.
The diagram source. Defaults to an empty string.
The lowercase diagram-language identifier. Defaults to mermaid.
The persisted presentation view. Defaults to split.
The type of diagram supported.
The view mode for code drawing elements.
Renders a diagram from code based on its language.
PlantUML requires an explicit rendering server. Mermaid, Graphviz, and Flowchart use their installed rendering libraries.
const image = await renderCodeDrawing('plantuml', source, {
plantUmlServer: 'https://www.plantuml.com/plantuml',
});const image = await renderCodeDrawing('plantuml', source, {
plantUmlServer: 'https://www.plantuml.com/plantuml',
});The copied CodeDrawingElement accepts a plantUmlServer prop and defaults to
https://www.plantuml.com/plantuml. Its Export button saves a PNG. Customize
the filename, preview height, and render debounce directly in the copied component.
import { BaseCodeDrawingPlugin } from 'platejs/code-drawing';
const insertItem = {
icon: <Code2Icon />,
label: 'Code Drawing',
value: 'code_drawing',
onSelect: () => {
const codeDrawing = editor.plugin(BaseCodeDrawingPlugin);
if (!codeDrawing.installed || editor.read.view.isReadOnly()) return;
codeDrawing.update.insert({}, { select: true });
},
};