Excalidraw is attractive because one React component provides a complete hand-drawn editor. The two failures that appear most often in real Next.js deployments are not drawing bugs: the editor is evaluated during server rendering, or its fonts, locales, and data chunks resolve from the wrong asset path.
A robust wrapper therefore starts with client-only loading and deterministic asset hosting. After that, the application can decide which native actions to expose, how to localize the editor, whether scenes remain ephemeral or persist, and how fullscreen behaves when the native browser API is unavailable.
Choose Excalidraw when the visual language fits
Excalidraw excels at architecture sketches, rough flows, workshop notes, and diagrams that should feel informal rather than pixel-perfect. It includes selection, connectors, text, images, scene loading, and image export. That makes it a strong embedded editor when the product does not need a custom shape engine.
A hand-drawn style is also a product decision. If users need multi-page canvases, a heavily customized UI, precise design tooling, or deep domain-specific shapes, compare the editor APIs before committing to the visual result alone.
| Requirement | Excalidraw fit |
|---|---|
| Hand-drawn flows and architecture sketches | Strong |
| Scene import and image export | Built in |
| Simple local single-user board | Strong |
| Custom collaboration backend | Possible, but separate architecture |
| Pixel-precise UI design editor | Usually not the target |
Keep the editor out of the server-rendering path
Use a client component and import @excalidraw/excalidraw/index.css from the route layout. In an effect, configure the asset base and then dynamically import the package. This avoids evaluating DOM-dependent code on the server and keeps the editor chunk off unrelated pages.
The asset path must be assigned before the runtime tries to resolve fonts, locales, or data chunks. A helper guarded by typeof window is safe to call at module initialization in the client bundle and again immediately before import.
const ASSET_PATH = '/excalidraw/';
function configureAssets() {
if (typeof window !== 'undefined') {
window.EXCALIDRAW_ASSET_PATH = ASSET_PATH;
}
}
useEffect(() => {
let active = true;
configureAssets();
import('@excalidraw/excalidraw').then(({ Excalidraw }) => {
if (active) setEditor(() => Excalidraw);
});
return () => { active = false; };
}, []);Copy and version the Excalidraw assets
EXCALIDRAW_ASSET_PATH should point to a directory served by the same deployment, such as /excalidraw/. Copy the version-matched package assets into public/excalidraw and keep the trailing slash. Test a clean production build because development resolution can hide missing files.
Open the browser Network panel and filter for failed fonts, locale modules, and data chunks. Strict CSP deployments should allow the editor’s required workers and blobs only as narrowly as the chosen feature set requires.
- Version package and copied assets together.
- Do not depend on node_modules paths at runtime.
- Verify Chinese and English locale chunks explicitly.
- Exercise image insertion and export after deployment, not only basic rectangles.
Expose native UI actions instead of rebuilding them
UIOptions controls which native canvas actions and tools are visible. A general local board can enable clear, scene loading, save-to-file, image export, background changes, theme switching, and image insertion. This provides a complete workflow without duplicating modal, file, and serialization behavior in the product shell.
Use initialData for a predictable first canvas state, but do not confuse it with controlled state. It initializes the scene; continuous persistence should be built through change callbacks or Excalidraw’s imperative API.
<Excalidraw
langCode={locale === 'zh' ? 'zh-CN' : 'en'}
name="product-sketch"
theme="light"
initialData={{
appState: {
viewBackgroundColor: '#ffffff',
currentItemStrokeColor: '#111827',
},
}}
UIOptions={{
canvasActions: {
clearCanvas: true,
loadScene: true,
saveAsImage: true,
toggleTheme: true,
},
tools: { image: true },
}}
/>Localize at the editor boundary
Map the application locale to Excalidraw’s langCode in one place. For example, zh becomes zh-CN while English uses en. The outer page title, privacy note, loading text, retry action, and fullscreen labels remain in the application’s message catalog.
This split keeps editor-owned strings aligned with the package translation while product-owned strings follow the website’s i18n workflow. Test the longest labels because the editor toolbar can wrap differently across locales.
Decide whether scenes are ephemeral, local, or synced
A minimal wrapper can rely on Excalidraw’s native load and save actions without retaining a copy in React state. That is a good privacy-first default for a utility page: the application does not collect the scene, and users explicitly save files when they want portability.
If automatic local restore is required, serialize elements, app state, and files carefully through onChange and store them in browser storage with debouncing and quota handling. If account sync or collaboration is required, file blobs, document permissions, encryption, migrations, and conflict handling become backend concerns.
- Do not write to storage on every pointer movement without debounce.
- Persist referenced files as well as scene elements or images will disappear.
- Strip transient app-state fields before long-term storage.
- Show users whether the board is unsaved, local-only, or synced.
Make fullscreen a product feature, not a CSS accident
The editor needs a container with an explicit height in embedded mode. For fullscreen, request native fullscreen on the wrapper and fall back to a fixed viewport container when the request rejects. Lock body scrolling in the fallback and release it during cleanup.
After entering or leaving either mode, dispatch resize so Excalidraw recalculates its viewport. Use min-h-0 on flex ancestors; without it, the canvas can overflow instead of shrinking inside a fullscreen column.
Handle loading and teardown deliberately
A blank white rectangle during a slow dynamic import looks broken. Render a neutral loading surface, switch to a localized error with retry if import fails, and cancel state updates after unmount. If the wrapper registers fullscreen or keyboard listeners, remove every listener and restore body styles in effect cleanup.
Keep analytics outside scene callbacks unless there is a clear, privacy-reviewed need. Tool usage can usually be measured with page and button events without collecting drawing elements, text, filenames, or embedded images.
Key takeaways
The reliable Excalidraw recipe is small but strict: client component, asset path before dynamic import, version-matched public assets, native UI composition, explicit persistence semantics, and a container that survives fullscreen changes. Most integration bugs disappear when those boundaries are designed first.