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 for viewing and editing. No accounts, no API keys, no rendering libraries -- just JSON.
Mandatory Rules
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.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.
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.
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
- Load this skill (you already did)
- Write the elements JSON -- an array of Excalidraw element objects, starting with a title
- Save the file using
write_fileto create a.excalidrawfile - 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:
{
"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:
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
cryptographypackage. 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:2roughness:1(hand-drawn look) -- see note belowopacity: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": 0on ALL elements. Hand-drawn wobble + dense text = unreadable mess.
Element Types
Rectangle:
{ "type": "rectangle", "id": "r1", "x": 100, "y": 100, "width": 200, "height": 100 }
roundness: { "type": 3 }for rounded cornersbackgroundColor: "#a5d8ff",fillStyle: "solid"for filled
Ellipse:
{ "type": "ellipse", "id": "e1", "x": 100, "y": 100, "width": 150, "height": 150 }
Diamond:
{ "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:
{ "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
containerIdis set - The text
x/y/width/heightare approximate -- Excalidraw recalculates them on load originalTextshould matchtext- Always include
fontFamily: 1(Virgil/hand-drawn font)
Labeled arrow -- same container binding approach:
{ "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):
{ "type": "text", "id": "t1", "x": 150, "y": 138, "text": "Hello", "fontSize": 20,
"fontFamily": 1, "strokeColor": "#1e1e1e", "originalText": "Hello", "autoResize": true }
xis the LEFT edge. To center at positioncx:x = cx - (text.length * fontSize * 0.5) / 2- Do NOT rely on
textAlignorwidthfor positioning
Arrow:
{ "type": "arrow", "id": "a1", "x": 300, "y": 150, "width": 200, "height": 0,
"points": [[0,0],[200,0]], "endArrowhead": "arrow" }
points:[dx, dy]offsets from elementx,yendArrowhead:null|"arrow"|"bar"|"dot"|"triangle"strokeStyle:"solid"(default) |"dashed"|"dotted"
Arrow Bindings (connect arrows to shapes)
{
"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
fontSizebelow 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
fontSizewide - 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
- Chinese characters are nearly square -- each char is roughly
- Mixed text: count CJK and ASCII separately, sum both widths, add 40px padding
Height:
- Single-line:
fontSize * 2 + 40 - Do NOT use multi-line
\ninside containers. Excalidraw's text reflow withcontainerIdis 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
\nmulti-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:
- 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.
- 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.
- Top-to-bottom flow. Arrange layers vertically so the eye scans naturally from top to bottom. Left-to-right within each layer.
- 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