Embed Excalidraw in Next.js: SSR, Save/Load
Stop fighting whiteboarding in React. Learn to embed Excalidraw with custom toolbar, server-side rendering hacks, API persistence, and theming – all in a production Next.js app.
Why Embed Excalidraw?
Excalidraw gives you a lightweight, collaborative whiteboard that renders shapes, text, and diagrams with a hand-drawn feel. But dropping it into a Next.js app introduces SSR conflicts, toolbar overreach, and data persistence challenges. This guide walks you through four exact steps: taming server-side rendering, building a custom toolbar, saving/loading drawings via an API, and matching your app's theme.
SSR Caveats
Next.js pre-renders pages on the server. Excalidraw depends on browser APIs like window, canvas, and PointerEvents. If you import it normally, you'll get a window is not defined error. Fix it with dynamic import and ssr: false.
- Wrap the component in a
dynamic()call fromnext/dynamic - Set
ssr: falseto skip server rendering - Mark the parent page or layout as a
'use client'component if you use React hooks nearby - Use
typeof window !== 'undefined'guards for any direct Excalidraw API calls
Example: Dynamic Import
import dynamic from 'next/dynamic';
const ExcalidrawWrapper = dynamic(
() => import('@excalidraw/excalidraw').then((mod) => mod.Excalidraw),
{ ssr: false }
);
export default function WhiteboardPage() {
return <ExcalidrawWrapper />;
}
This pattern alone handles the most common SSR pitfalls. For deeper hooks, ensure any callback passed to onChange or onPointerUpdate is stable (use useCallback) to avoid unnecessary re-renders.
Custom Toolbar
The default Excalidraw toolbar includes everything from laser pointers to image export. In a production app you often want only a subset. You can control the toolbar by using the ref and the updateScene API, or by hiding the built-in toolbar and building your own buttons.
- Set
viewModeEnabledtotrueto hide the default toolbar - Attach a ref to Excalidraw:
<Excalidraw ref={excalidrawRef} /> - Call
excalidrawRef.current?.updateScene({ appState: { activeTool: { type: 'rectangle' } } }) - Or use
setActiveToolfrom the API if your version exposes it
Example: Custom Rectangle Button
import { Excalidraw } from '@excalidraw/excalidraw';
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
return (
<>
<button onClick={() => {
excalidrawAPI?.setActiveTool({ type: 'rectangle' });
}}>
Rectangle
</button>
<Excalidraw
excalidrawAPI={(api) => setExcalidrawAPI(api)}
viewModeEnabled={false}
/>
</>
);
Map all the tools you need: selection, arrow, text, freedraw. For eraser, use type: 'eraser'. Keep your toolbar small – three to five buttons improve UX. Handle active tool state in a parent component so the UI stays in sync.
Save and Load via API
Users expect their drawings to persist. Excalidraw emits a onChange event with the full elements array and appState. You save this payload to your backend, then restore it on mount.
- Listen to
onChangeand debounce the save call (500ms works well) - Send a
POSTrequest to your API route with{ elements, appState } - Store it in your database – a single JSON column works
- On mount, fetch the saved data and pass it as
initialDataprop
Example: Save & Load
const [elements, setElements] = useState([]);
// Save (debounced)
const handleChange = useCallback((els, state) => {
setElements(els);
fetch('/api/whiteboard/save', {
method: 'POST',
body: JSON.stringify({ elements: els, appState: state }),
});
}, []);
// Load
useEffect(() => {
fetch('/api/whiteboard/load')
.then((res) => res.json())
.then((data) => {
if (data) {
setElements(data.elements);
}
});
}, []);
return (
<Excalidraw
initialData={{ elements, appState: null }}
onChange={handleChange}
/>
);
Keep in mind: appState can be large (view zoom, scroll position). You may want to save only elements and restore a minimal viewport. For large diagrams (over 500 elements), consider sending only changed elements using a diff approach.
Theming
Excalidraw ships with a theme prop that accepts 'light' or 'dark'. For deeper customization, you can override colors via UIOptions or CSS variables.
- Pass
theme={theme}wherethemeis from your app context (e.g.,'light'or'dark') - Use
UIOptionsto change canvas background:UIOptions={{ canvasBackground: '#f0f0f0' }} - Override CSS variables in your global styles:
--excalidraw-primary: #yourColor; - Match your app's brand colors for consistent UX
Example: Dark Mode Integration
const { colorMode } = useAppTheme(); // 'light' | 'dark'
<Excalidraw
theme={colorMode}
UIOptions={{
canvasBackground: colorMode === 'dark' ? '#1e1e1e' : '#ffffff',
}}
/>
For custom toolbar icons or dialog colors, inspect the Excalidraw CSS custom properties at runtime and override them in a :root block. This gives you a whiteboard that feels like part of your app, not a third-party widget.
You now have a production-ready embedding of Excalidraw in Next.js: SSR handled, toolbar tailored, data persisted, theme matched. The average implementation takes about two hours from start to finish. Ready to ship faster? Smartees gives you AI-powered code generation and real-time collaboration scaffolding so you can launch interactive whiteboards in minutes – not days.
Whiteboard
Free, browser-side, one sign-in for downloads.