CodeVetter · Repo Unpack · Feature catalog
excalidraw/excalidraw
The editor is a tool-driven canvas plus a large ActionName command set (clipboard, z-order, align, frames, text binding, zoom, theme, export). The host app layers collaboration rooms, encrypted share links, local autosave, PWA install, Excalidraw+ chrome, and two AI surfaces (text-to-diagram and diagram-to-code). Embedders get the same canvas through props, the imperative API, and the Next.js and browser-script examples.
- Pointer dispatch in handleCanvasPointerDown branches on activeTool.type: text, arrow/line, freedraw, custom, frame/magicframe, laser, autoshape, bucketfill, eraser, and a generic creator for rectangle-like tools. Element types include rectangle, diamond, ellipse, stickynote (baseHeight), embeddable, plus linear, image, text, and frame types in packages/element/src/types.ts.
- ActionName enumerates the command surface: copy/cut/paste, copyAsPng/copyAsSvg, z-index, align and distribute, flip, group, undo/redo, font and stroke properties, grid/zen/view/stats modes, hyperlink, cropEditor, frame membership, lock, linear editor, searchMenu, and commandPalette. Each action module calls register() so the side-effect import in App.tsx fills the actions array before ActionManager.registerAll.
- History is delta-based. Store emits durable increments for CaptureUpdateAction.IMMEDIATELY, never records NEVER (remote updates and init), and defers EVENTUALLY. History.record consumes those deltas; undo/redo are actions created with createUndoAction/createRedoAction and registered beside the static action list. History deltas deliberately drop version and versionNonce so a local undo still looks like a new collab edit.
- Export to disk uses serializeAsJSON, which writes type excalidraw, VERSIONS.excalidraw (2), source, elements, and appState. type local keeps files (minus those only referenced by deleted elements); type database sets files undefined so blobs are stored elsewhere. saveAsJSON uses the File System Access wrappers in data/filesystem.ts (browser-fs-access). PNG/SVG/clipboard exports are re-exported from @excalidraw/utils/export.
- Shape libraries persist through LibraryPersistenceAdapter (load/save). The app implements LibraryIndexedDBAdapter (idb-keyval store excalidraw-library) and LibraryLocalStorageMigrationAdapter for the legacy localStorage key. Remote library installs are restricted to hostnames excalidraw.com and raw.githubusercontent.com/excalidraw/excalidraw-libraries.
- i18n defaults to en from packages/excalidraw/locales/en.json. A locale is listed only if Crowdin coverage meets COMPLETION_THRESHOLD 85 (percentages.json). RTL is flagged for ar-SA, fa-IR, and he-IL. Crowdin syncs en.json to locales/%locale%.json. Vite manualChunks splits non-English locale JSON so the PWA precache can cache them separately.
- Live collaboration is app-only. ShareDialog calls collabAPI.startCollaboration(null) to create a room or stopCollaboration to leave. Links match /^#room=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/ and the key must be 22 characters or the UI alerts invalidEncryptionKey. The socket emits join-room and broadcasts SCENE_INIT, SCENE_UPDATE, MOUSE_LOCATION, IDLE_STATUS, and USER_VISIBLE_SCENE_BOUNDS.
- Readonly share links are a different hash: #json=<id>,<key>. exportToBackend generates an AES key, compresses serializeAsJSON(..., 'database'), POSTs the bytes to VITE_APP_BACKEND_V2_POST_URL, then writes the key only into location.hash. Image payloads are encrypted and uploaded under /files/shareLinks/${id} with a 4 MiB cap (FILE_UPLOAD_MAX_BYTES).
- Text-to-diagram lives in packages/excalidraw/components/TTDDialog. useTextGeneration accepts prompts of 3–10000 characters, streams via an AbortController, and parses mermaid with @excalidraw/mermaid-to-excalidraw. The host wires TTDIndexedDBAdapter (IDB key excalidraw-ttd-chats). Paste detection isMaybeMermaidDefinition recognizes flowchart, sequenceDiagram, C4Context, mindmap, and other mermaid headers.
- Diagram-to-code is a host plugin. DiagramToCodePlugin calls app.setPlugins({ diagramToCode }). AIComponents exports the selected frame to JPEG, collects text with getTextFromElements, and POSTs {texts, image, theme} to ${VITE_APP_AI_BACKEND}/v1/ai/diagram-to-code/generate-streaming, parsing the SSE body and treating HTTP 429 as a rate limit.
- Charts (bar, line, radar) are a paste path under packages/excalidraw/charts. Command palette UI is packages/excalidraw/components/CommandPalette/CommandPalette.tsx (fuzzy match over actions); defaultCommandPaletteItems.ts is an empty module. Analytics trackEvent only emits categories command_palette, export, and ai, and only when VITE_APP_ENABLE_TRACKING is the string true and window.sa_event exists.
- The hosted shell adds AppMainMenu, AppWelcomeScreen, AppSidebar, ShareDialog plus QR (uqr), language picker, Excalidraw+ promo and iframe export (VITE_APP_PLUS_APP / VITE_APP_PLUS_EXPORT_PUBLIC_KEY), and a beforeinstallprompt capture for PWA install. Collaboration is disabled when isRunningInIframe() is true. Examples with-nextjs dynamically imports the editor with ssr:false and sets window.EXCALIDRAW_ASSET_PATH.
- Embeddables and hyperlinks are host-gated. Excalidraw props include validateEmbeddable, renderEmbeddable, onLinkOpen, and generateLinkForSelection. SVG export leaves embeddables out unless renderEmbeddables is set. Links are passed through sanitizeUrl from @braintree/sanitize-url in packages/common/src/url.ts.