David Luzar e1b7aacb35 feat(packages/excalidraw): replace the export functions with 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)
2026-10-18 20:24:17 +02:00
2024-08-06 13:21:20 +02:00
2020-04-03 19:19:01 +02:00
2024-09-09 17:12:07 +02:00
2020-09-15 21:59:00 +02:00
2025-05-25 21:23:40 +02:00
2023-05-18 16:06:27 +02:00
2025-05-25 21:23:40 +02:00
2020-01-05 13:09:29 -08:00

Excalidraw

Excalidraw Editor | Blog | Documentation | Excalidraw+

An open source virtual hand-drawn style whiteboard.
Collaborative and end-to-end encrypted.


Excalidraw is released under the MIT license. npm downloads/month PRs welcome! Chat on Discord Ask DeepWiki Follow Excalidraw on Twitter

Product showcase

Create beautiful hand-drawn like diagrams, wireframes, or whatever you like.

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 .excalidraw json 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

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:

Vercel Sentry Crowdin

S
Description
Virtual whiteboard for sketching hand-drawn like diagrams
Readme MIT
582 MiB
Languages
TypeScript 94.9%
SCSS 2.5%
MDX 1.5%
JavaScript 1%