# Excalidraw

> Create hand-drawn style diagrams using Excalidraw. Preferred over Mermaid for all visual diagram needs. Use for architecture diagrams, flowcharts, user journeys, swimlanes, SWOT/matrix, roadmaps, timelines, org charts, mind maps, concept maps, and any scenario where the user asks to "draw" or "visualize" something. Trigger keywords — "画图", "画白板", "画流程图", "画架构图", "路线图", "脑图", "泳道图", "SWOT", "excalidraw", "diagram", "flowchart", "whiteboard", "roadmap", "mind map", "architecture".

- Skill: `lesliexqing/excalidraw` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add lesliexqing/excalidraw`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lesliexqing/excalidraw/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Product & Planning
- License: MIT
- Author: lesliexqing (https://skillmd.com/u/lesliexqing)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lesliexqing/excalidraw

---


# Excalidraw Diagram Skill

Create diagrams by writing standard Excalidraw element JSON and saving as `.excalidraw` files. These files can be drag-and-dropped onto [excalidraw.com](https://excalidraw.com) for viewing and editing. No accounts, no API keys, no rendering libraries -- just JSON.

## Mandatory Rules

1. **Title is REQUIRED.** Every diagram MUST have a standalone title text element as the FIRST element in the array. Use `fontSize: 28`, placed above all other content. The title should describe the diagram's subject.

2. **Match the user's language.** All text labels, titles, and annotations MUST use the same language as the user's prompt. If the user writes in Chinese, all text should be in Chinese. If in English, use English. Do NOT mix languages unless the user explicitly does so. Technical terms (e.g., "API", "MySQL", "Redis") may stay in English even in Chinese diagrams.

3. **Always upload and return a link.** After saving the file, upload it and return a clickable preview link. Do NOT just save the file and tell the user to open it manually.

4. **Output format.** Keep the response concise. The shareable link MUST be the LAST thing in your response, displayed prominently using a heading format. Do NOT put explanations, summaries, or feature lists after the link. Example output format:

```
（简要说明你画了什么，1-2句话）

## 👉 [点击在线预览和编辑](https://excalidraw.com/#json=xxx,yyy)

> 💡 在线预览中的编辑不会自动保存，修改后请及时点击左上角导出为 .excalidraw 文件留存。
```

## Workflow

1. **Load this skill** (you already did)
2. **Write the elements JSON** -- an array of Excalidraw element objects, starting with a title
3. **Save the file** using `write_file` to create a `.excalidraw` file
4. **Upload and return a shareable link** -- this step is **mandatory**, not optional

### Saving a Diagram

Wrap your elements array in the standard `.excalidraw` envelope and save with `write_file`:

```json
{
  "type": "excalidraw",
  "version": 2,
  "source": "excalidraw-skill",
  "elements": [ ...your elements array here... ],
  "appState": {
    "viewBackgroundColor": "#ffffff"
  }
}
```

Save to any path, e.g. `~/diagrams/my_diagram.excalidraw`.

### Uploading for a Shareable Link (REQUIRED)

After saving the .excalidraw file, you **must** upload it and return a preview link. Use the inline Python script below via terminal:

```python
import json, os, struct, sys, zlib, base64, urllib.request
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

def concat_buffers(*buffers):
    parts = [struct.pack(">I", 1)]
    for buf in buffers:
        parts.append(struct.pack(">I", len(buf)))
        parts.append(buf)
    return b"".join(parts)

file_path = "REPLACE_WITH_ACTUAL_PATH"  # <-- set this to the saved .excalidraw file path
with open(file_path, "r") as f:
    content = f.read()

inner = concat_buffers(json.dumps({}).encode(), content.encode())
compressed = zlib.compress(inner)
key = os.urandom(16)
iv = os.urandom(12)
encrypted = AESGCM(key).encrypt(iv, compressed, None)
meta = json.dumps({"version": 2, "compression": "pako@1", "encryption": "AES-GCM"}).encode()
payload = concat_buffers(meta, iv, encrypted)

req = urllib.request.Request("https://json.excalidraw.com/api/v2/post/", data=payload, method="POST")
resp = urllib.request.urlopen(req, timeout=30)
file_id = json.loads(resp.read())["id"]
key_b64 = base64.urlsafe_b64encode(key).rstrip(b"=").decode()
print(f"https://excalidraw.com/#json={file_id},{key_b64}")
```

Run this directly in terminal (replace the file path). It will print a shareable URL like:
```
https://excalidraw.com/#json=ABC123,encryptionKeyHere
```

**Return this link to the user as the final output.** The user can click to preview and edit the diagram directly in the browser.

> How it works: The diagram is AES-GCM encrypted client-side before upload. The decryption key lives in the URL `#` fragment (never sent to server), so excalidraw.com never sees your plaintext data.
>
> Requires `cryptography` package. Install with: `pip install cryptography`

---

## Element Format Reference

### Required Fields (all elements)
`type`, `id` (unique string), `x`, `y`, `width`, `height`

### Defaults (skip these -- they're applied automatically)
- `strokeColor`: `"#1e1e1e"`
- `backgroundColor`: `"transparent"`
- `fillStyle`: `"solid"`
- `strokeWidth`: `2`
- `roughness`: `1` (hand-drawn look) -- see note below
- `opacity`: `100`

Canvas background is white.

### Roughness (line style)
- `roughness: 1` — hand-drawn/sketchy look (default). Good for simple diagrams with few elements.
- `roughness: 0` — clean, straight lines. **Use this for complex diagrams (10+ elements), diagrams with long text labels, or when clarity matters more than aesthetics.**

> **Rule of thumb:** If the diagram has more than ~10 labeled shapes, set `"roughness": 0` on ALL elements. Hand-drawn wobble + dense text = unreadable mess.

### Element Types

**Rectangle**:
```json
{ "type": "rectangle", "id": "r1", "x": 100, "y": 100, "width": 200, "height": 100 }
```
- `roundness: { "type": 3 }` for rounded corners
- `backgroundColor: "#a5d8ff"`, `fillStyle: "solid"` for filled

**Ellipse**:
```json
{ "type": "ellipse", "id": "e1", "x": 100, "y": 100, "width": 150, "height": 150 }
```

**Diamond**:
```json
{ "type": "diamond", "id": "d1", "x": 100, "y": 100, "width": 150, "height": 150 }
```

**Labeled shape (container binding)** -- create a text element bound to the shape:

> **WARNING:** Do NOT use `"label": { "text": "..." }` on shapes. This is NOT a valid
> Excalidraw property and will be silently ignored, producing blank shapes. You MUST
> use the container binding approach below.

The shape needs `boundElements` listing the text, and the text needs `containerId` pointing back:
```json
{ "type": "rectangle", "id": "r1", "x": 100, "y": 100, "width": 200, "height": 80,
  "roundness": { "type": 3 }, "backgroundColor": "#a5d8ff", "fillStyle": "solid",
  "boundElements": [{ "id": "t_r1", "type": "text" }] },
{ "type": "text", "id": "t_r1", "x": 105, "y": 110, "width": 190, "height": 25,
  "text": "Hello", "fontSize": 20, "fontFamily": 1, "strokeColor": "#1e1e1e",
  "textAlign": "center", "verticalAlign": "middle",
  "containerId": "r1", "originalText": "Hello", "autoResize": true }
```
- Works on rectangle, ellipse, diamond
- Text is auto-centered by Excalidraw when `containerId` is set
- The text `x`/`y`/`width`/`height` are approximate -- Excalidraw recalculates them on load
- `originalText` should match `text`
- Always include `fontFamily: 1` (Virgil/hand-drawn font)

**Labeled arrow** -- same container binding approach:
```json
{ "type": "arrow", "id": "a1", "x": 300, "y": 150, "width": 200, "height": 0,
  "points": [[0,0],[200,0]], "endArrowhead": "arrow",
  "boundElements": [{ "id": "t_a1", "type": "text" }] },
{ "type": "text", "id": "t_a1", "x": 370, "y": 130, "width": 60, "height": 20,
  "text": "connects", "fontSize": 16, "fontFamily": 1, "strokeColor": "#1e1e1e",
  "textAlign": "center", "verticalAlign": "middle",
  "containerId": "a1", "originalText": "connects", "autoResize": true }
```

**Standalone text** (titles and annotations only -- no container):
```json
{ "type": "text", "id": "t1", "x": 150, "y": 138, "text": "Hello", "fontSize": 20,
  "fontFamily": 1, "strokeColor": "#1e1e1e", "originalText": "Hello", "autoResize": true }
```
- `x` is the LEFT edge. To center at position `cx`: `x = cx - (text.length * fontSize * 0.5) / 2`
- Do NOT rely on `textAlign` or `width` for positioning

**Arrow**:
```json
{ "type": "arrow", "id": "a1", "x": 300, "y": 150, "width": 200, "height": 0,
  "points": [[0,0],[200,0]], "endArrowhead": "arrow" }
```
- `points`: `[dx, dy]` offsets from element `x`, `y`
- `endArrowhead`: `null` | `"arrow"` | `"bar"` | `"dot"` | `"triangle"`
- `strokeStyle`: `"solid"` (default) | `"dashed"` | `"dotted"`

### Arrow Bindings (connect arrows to shapes)

```json
{
  "type": "arrow", "id": "a1", "x": 300, "y": 150, "width": 150, "height": 0,
  "points": [[0,0],[150,0]], "endArrowhead": "arrow",
  "startBinding": { "elementId": "r1", "fixedPoint": [1, 0.5] },
  "endBinding": { "elementId": "r2", "fixedPoint": [0, 0.5] }
}
```

`fixedPoint` coordinates: `top=[0.5,0]`, `bottom=[0.5,1]`, `left=[0,0.5]`, `right=[1,0.5]`

### Drawing Order (z-order)
- Array order = z-order (first = back, last = front)
- Emit progressively: background zones → shape → its bound text → its arrows → next shape
- BAD: all rectangles, then all texts, then all arrows
- GOOD: bg_zone → shape1 → text_for_shape1 → arrow1 → arrow_label_text → shape2 → text_for_shape2 → ...
- Always place the bound text element immediately after its container shape

### Sizing Guidelines

**Font sizes:**
- Minimum `fontSize`: **16** for body text, labels, descriptions
- Minimum `fontSize`: **20** for titles and headings
- Minimum `fontSize`: **14** for secondary annotations only (sparingly)
- NEVER use `fontSize` below 14

**Container sizing (CRITICAL -- #1 cause of ugly diagrams):**

Text inside a shape WILL wrap and overflow if the container is too small. You MUST size containers based on text length.

**Width formula — depends on character type:**
- **English text:** `width = max(160, char_count * fontSize * 0.65)`
- **Chinese/CJK text:** `width = max(160, char_count * fontSize * 1.1)`
  - Chinese characters are nearly square -- each char is roughly `fontSize` wide
  - Example: "API 网关" (5 chars, 3 CJK + 2 ASCII) at fontSize 16 → width = max(160, 3×16×1.1 + 2×16×0.65) = **173px** → use 180
  - Example: "认证服务" (4 CJK chars) at fontSize 16 → width = max(160, 4×16×1.1) = **160px**
- **Mixed text:** count CJK and ASCII separately, sum both widths, add 40px padding

**Height:**
- Single-line: `fontSize * 2 + 40`
- **Do NOT use multi-line `\n` inside containers.** Excalidraw's text reflow with `containerId` is unreliable for multi-line, especially with CJK. Keep all container labels to a **single line**. If the label is too long, abbreviate it or make the container wider.
- If you absolutely must use `\n`: height = `(line_count * fontSize * 1.5) + 40`, and test carefully.

**Minimum sizes:**
- Labeled rectangles/ellipses: **160×70** minimum
- Diamonds (decision nodes): **180×120** minimum (text renders smaller inside diamonds)
- Leave **40-60px** gaps between elements (not 20px -- too tight for complex diagrams)
- Prefer fewer, larger elements over many tiny ones

> **Common mistakes:**
> - Setting all shapes to the same size regardless of text length → overlapping text
> - Using English width formula for Chinese text → containers too narrow, text overflows
> - Using `\n` multi-line text inside containers → text overlaps and misaligns

### Layout Strategy (information density)

Do NOT flatten all elements into a single flat grid. Use **visual zoning** to create hierarchy:

1. **Group by layers, not by type.** Divide elements into logical layers (e.g., user layer → logic layer → data layer → infra layer). Each layer gets its own background zone rectangle.
2. **Big things get big space, small things compress.** Main concepts get large boxes; secondary items can be compressed into smaller rows or color strips at the bottom.
3. **Top-to-bottom flow.** Arrange layers vertically so the eye scans naturally from top to bottom. Left-to-right within each layer.
4. **Background zones define groups.** Use large semi-transparent rectangles (`opacity: 30-35`) behind each layer. This instantly communicates grouping without needing to read every label.

### Color Strategy

**Use colors to represent "layers", not "categories".** Fix a consistent mapping across the whole diagram:
- Blue = user/input layer
- Purple = logic/processing layer  
- Green = data/output/execution layer
- Yellow = notes/annotations
- Orange = external/warning
- Red = errors/critical

Once fixed, readers can scan by color to understand structure without reading every label. **Do NOT use random colors for decoration.**

**Always use pastel fills + dark strokes.** Never use high-saturation solid fills — they clash with the hand-drawn style and make text hard to read. See `references/colors.md` for the full palette.

### Color Palette

See `references/colors.md` for full color tables. Quick reference:

| Use | Fill Color | Hex |
|-----|-----------|-----|
| Primary / Input | Light Blue | `#a5d8ff` |
| Success / Output | Light Green | `#b2f2bb` |
| Warning / External | Light Orange | `#ffd8a8` |
| Processing / Special | Light Purple | `#d0bfff` |
| Error / Critical | Light Red | `#ffc9c9` |
| Notes / Decisions | Light Yellow | `#fff3bf` |
| Storage / Data | Light Teal | `#c3fae8` |

### Tips
- Use the color palette consistently across the diagram
- **Text contrast is CRITICAL** -- never use light gray on white backgrounds. Minimum text color on white: `#757575`
- Do NOT use emoji in text -- they don't render in Excalidraw's font
- **Complex diagrams (10+ nodes):** always use `roughness: 0`, wider containers, and 40-60px spacing
- **Keep labels short** -- abbreviate where possible. "CI/CD" not "Continuous Integration / Continuous Deployment"
- **Arrow labels: 3-5 characters max.** Arrows only carry a keyword for the connection (e.g., "调用", "数据", "HTTP"). Detailed descriptions go inside shapes, not on arrows.
- **Align elements on a grid** -- use consistent x/y coordinates across rows and columns to keep layout clean
- For dark mode diagrams, see `references/dark-mode.md`
- For larger examples, see `references/examples.md`



