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: parsed in frontmatter) — JSON is a human-readable # Drawing code block
  • compressed (default after first save) — JSON is LZ-String compressed in a # Excalidraw Data / ## Drawing / ```compressed-json block

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: null for sharp, { "type": 3 } for rounded rectangles
  • boundElements: 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's id so the text "belongs" to it
  • The container's boundElements must include { "type": "text", "id": "<text-id>" }
  • baselinefontSize * 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) origin
  • startBinding / endBinding: connect to rectangles; focus: 0 = center, gap: 2 = small pixel gap before arrowhead
  • Both source and target rectangles' boundElements must 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.