exportScene (#12347)
* [export] rewrite - init * restore elements + filter deleted in export API (restores #11470 behavior) * fix snaps * port export callers added on master to the new API + snaps - renderOverrides (#12100), sticky notes (#12064) and arrow-label hole (#10947) tests called the old positional exportToSvg/exportToCanvas - embedded-scene snapshots gain master's new element fields (created, baseFontSize, labelPosition); decoded payloads match master's Claude Opus 5.5 (xhigh) * restore elements in embedded scene + json clipboard export (restores #12060 behavior) - master's #12060 made every public export entry point restore its input (legacy elements reach lower-level code with `created: null`), incl. the PNG metadata embed and the json clipboard copy in the removed @excalidraw/utils wrappers - SVG embed now serializes the restored, non-deleted elements (same as master's utils path), PNG embed serializes restoreElements(data.elements) and json clipboard copies the non-deleted restored elements - port #12060's utils tests to the new API, observing renderers/serializer instead of the (now identical) scene export functions Claude Opus 5.5 (xhigh) * make config.canvasBackgroundColor the single export background knob - background resolved once in configExportDimension; exportToSvg no longer reads appState.exportBackground (now omitted from ExportSceneData's appState type), and honors `canvasBackgroundColor: false` - outline arrowheads (circle/triangle/diamond_outline, cardinality_zero_*) are filled with the resolved background color, also when the background isn't painted (as on master) — the canvas path filled them transparent, showing the arrow's shaft through them; SVG ignored the config color - JPEG: `canvasBackgroundColor: false` falls back to the default background (+ warning) instead of rendering black; no more warning for the default - callers (exportCanvas, ImageExportDialog, library/chart/publish previews, AI) pass the background via config only Claude Opus 5.5 (xhigh) * resolve export theme before creating frame labels - frame labels were colored by appState.exportWithDarkMode, ignoring config.theme (export dialog's dark toggle), and in dark exports the dark-theme label color got dark-filtered again by the renderer (#7a7a7a -> #848484) - theme is now resolved once in configExportDimension; dark labels are pre-reverted so they render with the editor's frame name color Claude Opus 5.5 (xhigh) * drop appState.exportWithDarkMode theme fallback in export - `config.theme` is the only theme input (defaults to light); the type already omitted `exportWithDarkMode` from `data.appState` - callers that relied on the fallback (PublishLibrary item preview, AI frame export) pass `theme` explicitly, keeping their behavior Claude Opus 5.5 (xhigh) * exportToClipboard: don't restore appState before exportToSvg (already restored there) Claude Opus 5.5 (xhigh) * remove unused getExportSize, type getCanvasSize's [x, y, width, height] (not Bounds) Claude Opus 5.5 (xhigh) * fix typos in export comments/JSDoc Claude Opus 5.5 (xhigh) * port with-script-in-browser example to the new export API - exportToSvg/Blob/Canvas/Clipboard take `{ data, config }`; theme via `config.theme`, padding 10 kept explicit (new default is 0) - dropped the no-op `width`/`height` appState overrides from the SVG export Claude Opus 5.5 (xhigh) * call getDimensions once, ignore it when widthOrHeight is set - with an explicit `fit: "contain"`, getDimensions ran twice (the second time on its own output); it now supplies width/height (+ scale) once, and `contain` scales the content into that frame - getDimensions + widthOrHeight both applied; widthOrHeight now wins (with a warning), like width/height/maxWidthOrHeight already did - `fit: "none"` (the E+ export server path) renders identically Claude Opus 5.5 (xhigh) * type the export size options as mutually exclusive - `width`/`height`, `maxWidthOrHeight`, `widthOrHeight` and `getDimensions` are now a union where the unused options are `?: never`, so combining them is a type error (runtime still ignores/warns for JS callers) - the `never` keys are remapped over `keyof`, so every option keeps its JSDoc in completions & hovers (verified via the TS language service) Claude Opus 5.5 (xhigh) * replace getDimensions with a `scale` function, round the canvas size - `config.scale` also accepts `(width, height) => number`, called with the export dimensions at scale 1 (padding included) — covers content-dependent scaling (e.g. E+'s pixel budgets) without the getDimensions round trip - remove `getDimensions`: its returned `scale` replaced `config.scale` and multiplied the returned size, so callers thinking in output pixels had to divide it back out - exportToCanvas rounds the canvas size instead of letting the canvas truncate it (float noise like 28.999999999999996 lost a pixel); half pixels now round up (frame label area 20.5px, mermaid preview 89.5px) Claude Opus 5.5 (xhigh) * align fit/padding/position export docs with the code - `fit` defaults to "contain" when `width`/`height` is set, otherwise "none" (the doc said the opposite); dropped the unimplemented "x/y force none" claim - padding is always subtracted for `maxWidthOrHeight`/`widthOrHeight` (they ignore `fit`), not added - `position` values are "center" | "topLeft" (doc listed "none") Claude Opus 5.5 (xhigh) * derive a missing export width/height from the aspect ratio - with `fit: "contain"` (the default when a size is given), passing only `width` or `height` now derives the other from the content's aspect ratio, as documented — it defaulted to the content's unscaled size, e.g. `{ height: 50 }` of 200x100 content gave a 200x50 canvas with 60x30 content in wide empty bands (now 80x50 with padding 10) - with `fit: "none"` the missing side stays the content extent Claude Opus 5.5 (xhigh) * default export `fit` to "none" when x/y is set - `{ x, y, width, height }` was scaled to fit with `x`/`y` silently ignored (`fit` defaulted to "contain" whenever a size was given); an explicit region now crops by default, `fit: "contain"` still opts in (and ignores `x`/`y`, as now documented) Claude Opus 5.5 (xhigh) * ignore export x/y when the content is scaled to fit - with `fit: "contain"`, `maxWidthOrHeight` or `widthOrHeight`, `x`/`y` shifted the fitted content by a mix of scene units and pixels; they're now dropped with a warning, which the previous commit already documented (its "ignored" claim was based on a coincidental probe) Claude Opus 5.5 (xhigh) * pad an export x/y region on all sides - with `position: "center"` (the default), the centering adjustment also ran on axes pinned by `x`/`y`, shifting the region by the padding: the canvas started exactly at `x`/`y` with all padding on the right/bottom (`position: "topLeft"` padded all sides); `position` now only applies to unpinned axes, as documented Claude Opus 5.5 (xhigh) * consolidate exportToCanvas/Blob/Svg into exportScene({ type, data, config }) - `type: "canvas" | "png" | "jpeg" | "svg"`, return type per `type` (overloads, plus a union overload for dynamic callers); per-type options are type-checked: `quality` only for jpeg, `embedScene` for png/svg, `renderEmbeddables`/`skipInliningFonts`/`reuseImages` for svg - the format replaces `mimeType` (and its "image/jpg" typo handling) - `appState.exportEmbedScene` -> `config.embedScene` (no longer read from `data.appState`); `data.files` is optional - `config.createCanvas` -> `config.environment.createCanvas` (where #11997's render environment and richtext's text metrics will go) - exportToCanvas/Blob/Svg are no longer exported (no aliases); exportToClipboard stays, routed through exportScene - callers, the with-script-in-browser example and tests ported Claude Opus 5.5 (xhigh) * export: take only scene settings from data.appState, frameRendering -> config.frames - `data.appState` is typed as (and stripped at runtime to) the scene settings a .excalidraw file stores; the renderer state is built from those + defaults, so editor state passed in (a live AppState: open dialog, cropping, frame highlight…) can't affect the output. It only supplies the default background and what `embedScene` embeds - `appState.frameRendering` -> `config.frames: { name, outline, clip }` - editor callers translate their export settings via one internal `getExportConfigFromAppState`; `exportCanvas` drops its redundant exportBackground/viewBackgroundColor options - fixes the editor's SVG export ignoring `exportScale` (the branch's exportCanvas SVG path had dropped it; master passed it) Claude Opus 5.5 (xhigh) * type export positioning options as width/height-only - `fit`, `position`, `x`, `y`, `origin` move into the size-option union's `width`/`height` member, so combining them with `maxWidthOrHeight` / `widthOrHeight` (which ignore them) is a type error; runtime unchanged (still ignores/warns for JS callers) - docs stay visible in completions (same `keyof` remap as the size options) Claude Opus 5.5 (xhigh) * move export `loadFonts` into `config.environment` - font loading is a host capability, next to `createCanvas` (svg gets `environment: { loadFonts }` only, as it creates no canvas) Claude Opus 5.5 (xhigh) * changelog: export API (exportScene, config, migration table) Claude Opus 5.5 (xhigh) * export: default padding back to 10, canvas at least 1px - export bounds are geometric (no stroke width), so padding 0 cut half of the strokes on the content's edge and made flat content (a line) zero-height — the library/chart/publish previews, which inherited master's default of 10, rendered blank thumbnails for lines - canvas dimensions never go below 1px (a 0-sized canvas can't be encoded); SVG snapshot sizes are back to master's Claude Opus 5.5 (xhigh) * export: clamp padding against the fitted size, before deriving from it - the padding clamp used the content's unpadded size, so flat content (a horizontal line) lost its padding with maxWidthOrHeight/widthOrHeight (`{ padding: 10, maxWidthOrHeight: 400 }` → 200x0); it now only considers the target being fitted into - a missing width/height was derived before clamping (`{ height: 10, padding: 10 }` → 0x10, SVG viewBox NaN/Infinity) - a zero-extent content axis no longer yields a NaN scale under contain Claude Opus 5.5 (xhigh) * export: clip frame exports to the frame - frame clipping was off for exportingFrame (to avoid the rounded per-element clip), relying on the canvas/SVG edge — which only crops when the export is exactly the frame; a larger export (size options) painted children and overlapping elements into the margins, and SVG frame exports had stopped clipping (master clipped them) - everything drawn is now clipped to the frame with square corners: a canvas clip after the background (`renderConfig.clipToFrame`), and an SVG group with the frame's clip path; the background isn't clipped Claude Opus 5.5 (xhigh) * changelog: fill in PR numbers for the placeholder links Claude Opus 5.5 (xhigh)
Excalidraw Editor | Blog | Documentation | Excalidraw+
An open source virtual hand-drawn style whiteboard.
Collaborative and end-to-end encrypted.
Features
The Excalidraw editor (npm package) supports:
- 💯 Free & open-source.
- 🎨 Infinite, canvas-based whiteboard.
- ✍️ Hand-drawn like style.
- 🌓 Dark mode.
- 🏗️ Customizable.
- 📷 Image support.
- 😀 Shape libraries support.
- 🌐 Localization (i18n) support.
- 🖼️ Export to PNG, SVG & clipboard.
- 💾 Open format - export drawings as an
.excalidrawjson file. - ⚒️ Wide range of tools - rectangle, circle, diamond, arrow, line, free-draw, eraser...
- ➡️ Arrow-binding & labeled arrows.
- 🔙 Undo / Redo.
- 🔍 Zoom and panning support.
Excalidraw.com
The app hosted at excalidraw.com is a minimal showcase of what you can build with Excalidraw. Its source code is part of this repository as well, and the app features:
- 📡 PWA support (works offline).
- 🤼 Real-time collaboration.
- 🔒 End-to-end encryption.
- 💾 Local-first support (autosaves to the browser).
- 🔗 Shareable links (export to a readonly link you can share with others).
We'll be adding these features as drop-in plugins for the npm package in the future.
Quick start
Note: following instructions are for installing the Excalidraw npm package when integrating Excalidraw into your own app. To run the repository locally for development, please refer to our Development Guide.
Use npm or yarn to install the package.
npm install react react-dom @excalidraw/excalidraw
# or
yarn add react react-dom @excalidraw/excalidraw
Check out our documentation for more details!
Contributing
- Missing something or found a bug? Report here.
- Want to contribute? Check out our contribution guide or let us know on Discord.
- Want to help with translations? See the translation guide.
Integrations
Who's integrating Excalidraw
Google Cloud • Meta • CodeSandbox • Obsidian Excalidraw • Replit • Slite • Notion • HackerRank • and many others
Sponsors & support
If you like the project, you can become a sponsor at Open Collective or use Excalidraw+.
Thank you for supporting Excalidraw
Last but not least, we're thankful to these companies for offering their services for free: