* refactor(editor): move the drag-pan out of App.tsx into App.pan.ts `handleCanvasPanUsingWheelOrSpaceDrag` — gating, the Linux middle-click paste guard, the throttled move and the teardown — moves as-is into an `AppPan` helper (`app.pan`), in the shape of `App.cursor` and `App.viewport`. The session state it ran on was module-level in App.tsx (`isHoldingSpace`, `isPanning`, `flushPanMove`), shared by every `App` instance in the page: with two editors mounted, a wheel-button pan in one made the other ignore wheel input. It is instance state now — `spaceHeld`, `active`, the pending move flush and the teardown — behind `start`, `isActive`, `flushMove`, `end` and the space accessors. Only the pointer count is injected; `gesture` stays where it is. `AppWheel` asks `app.pan` directly instead of taking those two callbacks as dependencies, the wysiwyg's wheel-button pan calls `app.pan.start`, and `maybeCleanupAfterMissingPointerUp` ends the pan explicitly, which leaves `lastPointerUp` to the scrollbar drag alone. `savePointer` becomes public for the teardown. No behavior change. Claude Fable 5.1, high effort * feat(editor): pan by dragging with the secondary mouse button A secondary-button press starts a pan session like the wheel button does, except that it stays a right-click until the pointer travels past the drag threshold: nothing moves and the cursor stays put before that; past it the canvas follows the pointer (from that point, the threshold distance is not caught up) and no menu opens. Released before the threshold, the session opens the context menu itself at the release point, through the same `openContextMenu` the `contextmenu` handler now delegates to. That handler ignores the platform's `contextmenu` event while a secondary-button session is active — macOS and Linux fire it on mousedown, before a drag can be told from a click — and once after the session ends if it has not fired yet, which is Windows firing it on mouseup. Touch long-press, pen barrel taps and keyboard-triggered menus never go through a session, so they open as before. On macOS and Linux the menu now opens on release rather than on press. View mode already let any button pan; a right-click there opens the view-mode menu on release the same way. With interaction disabled the drag still pans (when navigation is enabled) and no menu opens. Tests: the threshold, the menu on release, both platform orders of the native event, no menu after a drag, element selection under the pointer, and the non-interactive editor. Claude Fable 5.1, high effort * fix(editor): let the contextmenu after mouseup open the menu for a right-click, as before The secondary-button session opened the menu on release everywhere. On Windows the platform's `contextmenu` fires after mouseup, by which time our menu was rendered under the pointer; the event then targeted the popover instead of the canvas, the canvas handler that prevented the native menu never ran, and the native menu opened on top of ours. The session now opens the menu on release only where the platform's `contextmenu` already came with the press (macOS, Linux) and was swallowed because a drag could not be told from a click yet. Where it follows the release, it opens the menu itself, exactly as before this branch, and lands on the canvas as there is nothing under the pointer. After a drag that event is swallowed once, as it was. Tests: after mouseup, nothing is open at release and the platform's event opens the menu; the mousedown-order and drag cases are unchanged. Claude Fable 5.1, high effort * reintroduce: ctrl+wheel should always zoom for both input devices * fix(editor): lower right-button pan threshold to 5px * fix(editor): broadcast the final pointer position after panning * fix(editor): suppress cursor broadcasts while panning
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: