app-dev/obsidian-excalidraw/references/format-gotchas.md
Excalidraw File Format — Deep Reference
The Obsidian Excalidraw plugin uses .excalidraw.md files — markdown with an embedded JSON scene. The plugin stores the scene in one of two modes:
- parsed (
excalidraw-plugin: parsedin frontmatter) — JSON is a human-readable# Drawingcode block - compressed (default after first save) — JSON is LZ-String compressed in a
# Excalidraw Data/## Drawing/```compressed-jsonblock
When generating a file from scratch, always author in parsed mode. The plugin rewrites it to compressed on first save; don't try to re-author compressed output.
Required structure (parsed mode)
---
excalidraw-plugin: parsed
tags:
- excalidraw
---
==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠==
# Drawing
```json
{ ...scene... }
%%
- The `==⚠ ... ⚠==` highlight line is the fallback shown in reading view when the user hasn't enabled Excalidraw View for this file
- The closing `%%` is required — it wraps the `# Drawing` section as an Obsidian comment so the JSON doesn't render as visible markdown
## Element IDs must be random — never semantic
Element `id` values are reused by the plugin as block-reference anchors in the auto-generated `## Text Elements` section:
Acme Studio ^studio-txt Billing ^billing-txt
If your IDs are short and predictable (`studio`, `billing-txt`, `title`, `edge-1`), the plugin's anchor parser mis-matches — anchors collide with prefixes of other anchors, and the parser concatenates consecutive text entries into one element's `text` field. Result: one box renders "Title ^title
Title ^title
Acme Studio" and an "Excalidraw ran into an unknown problem!" dialog appears.
**Always use random 8-char base62 IDs** like the plugin generates itself (e.g., `aB3xK9mZ`, `fHjKlMn3`). The builder script does this — don't undo it if you edit the script.
## The Text Elements gotcha (the #1 way to break a file)
After a file is loaded, the plugin regenerates a `## Text Elements` (or `# Text Elements`) section listing every text-type element with a `^blockId` anchor:
Text Elements
Studio DynamoDB ^abc12345
• accounts • account-brands ^def67890
Each `^id` anchor must correspond 1:1 to the `id` field of a real text element in the JSON. If you hand-author the Text Elements section with mismatched anchors, the plugin's parser **concatenates consecutive text entries** and dumps the result into whichever element it can match. Your clean title "Studio DynamoDB" becomes the multi-line blob `"Studio DynamoDB
• accounts
• account-brands"`, which renders as a wall of overlapping text.
**Always omit the Text Elements section when generating a file.** The plugin auto-generates it correctly on first load.
## Scene JSON shape
```json
{
"type": "excalidraw",
"version": 2,
"source": "https://github.com/zsviczian/obsidian-excalidraw-plugin",
"elements": [ ... ],
"appState": {
"gridSize": null,
"viewBackgroundColor": "#ffffff"
},
"files": {}
}
Common element types
Rectangle
{
"id": "r1",
"type": "rectangle",
"x": 100, "y": 100, "width": 200, "height": 80,
"angle": 0,
"strokeColor": "#1971c2",
"backgroundColor": "#a5d8ff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"roundness": { "type": 3 },
"seed": 1,
"version": 1,
"versionNonce": 1,
"isDeleted": false,
"boundElements": [{ "type": "text", "id": "t1" }, { "type": "arrow", "id": "a1" }],
"updated": 1744916000000,
"link": null,
"locked": false
}
fillStyle:"solid","hachure","cross-hatch","zigzag"strokeStyle:"solid","dashed","dotted"roughness:0(architect, clean),1(artist, default),2(cartoonist)roundness:nullfor sharp,{ "type": 3 }for rounded rectanglesboundElements: array of references to text elements inside and arrows attached
Text bound to a container
{
"id": "t1",
"type": "text",
"x": 110, "y": 125, "width": 180, "height": 30,
"text": "Hello",
"originalText": "Hello",
"fontSize": 20,
"fontFamily": 1,
"textAlign": "center",
"verticalAlign": "middle",
"baseline": 18,
"lineHeight": 1.25,
"containerId": "r1",
"autoResize": true,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
...
}
fontFamily:1(Cascadia, monospace),2(Virgil, handwritten),3(Helvetica)containerId: set to the rectangle'sidso the text "belongs" to it- The container's
boundElementsmust include{ "type": "text", "id": "<text-id>" } baseline≈fontSize * 0.9(used for vertical text metrics)
Arrow with bindings
{
"id": "a1",
"type": "arrow",
"x": 300, "y": 160,
"width": 120, "height": 0,
"points": [[0, 0], [120, 0]],
"lastCommittedPoint": null,
"startBinding": { "elementId": "r1", "focus": 0, "gap": 2 },
"endBinding": { "elementId": "r2", "focus": 0, "gap": 2 },
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": false,
"roundness": { "type": 2 },
...
}
points:[[startX, startY], [endX, endY]]relative to the arrow's own(x, y)originstartBinding/endBinding: connect to rectangles;focus: 0= center,gap: 2= small pixel gap before arrowhead- Both source and target rectangles'
boundElementsmust reference this arrow:{ "type": "arrow", "id": "a1" } startArrowhead:null(no tail arrowhead) or"arrow"/"triangle"endArrowhead: typically"arrow"strokeStyle: "dashed"for dashed relations
View preferences in appState
{
"gridSize": 20, // null = no grid
"viewBackgroundColor": "#ffffff",
"currentItemFontFamily": 1,
"currentItemStrokeWidth": 2,
"theme": "light" // or "dark"
}
Most appState fields are optional — the plugin fills in defaults.
Embedding diagrams in notes
In any Obsidian note:
![[Architecture Overview.excalidraw]] # renders as a static SVG preview
![[Architecture Overview.excalidraw|600]] # preview at 600px wide
Click the embed to open the full interactive view.
Migrating legacy .excalidraw files (JSON-only, no markdown)
If you have a pre-2022 .excalidraw file (pure JSON, no markdown wrapper), just paste its JSON content into a new .excalidraw.md file's # Drawing block. The plugin handles the rest on first load.
