
Excalidraw
- 3.1k installs
- 29 repo stars
- Updated August 2, 2026
- agents365-ai/365-skills
excalidraw generates .excalidraw diagrams and exports PNG or SVG via Kroki or local CLI.
About
The excalidraw skill generates .excalidraw JSON diagrams and exports to PNG or SVG for architecture, flowcharts, and system visualizations. Kroki API via curl provides zero-install SVG export; excalidraw-brute-export-cli with Firefox enables local PNG and SVG. Workflow checks dependencies, plans visual metaphor and palette, writes excalidraw JSON section by section for large diagrams, exports, verifies render quality, and loops minimal edits until approved. Design principles favor roughness 0 clean lines, Helvetica fontFamily 2, typography over boxes with under thirty percent of text inside rectangles, 60-30-10 color rule from a fixed semantic palette, and arrow semantics for solid primary, dashed async, and dotted optional flows. Community libraries via excalidraw_lib.py merge vector AWS, Azure, and network icons without image elements. Route polished UML to drawio, diagrams-as-code to mermaid, and infinite canvas needs to tldraw instead.
- Kroki curl for SVG; local CLI with Firefox for PNG export.
- Prefer free-floating text over boxing every label.
- Fixed semantic palette; do not invent new colors.
- excalidraw_lib.py merges vector community icons at x,y.
- Verify render, review loop, re-export until user approves.
Excalidraw by the numbers
- 3,125 all-time installs (skills.sh)
- +171 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #127 of 1,879 Documentation skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
excalidraw capabilities & compatibility
- Capabilities
- excalidraw json authoring with design system rul · kroki svg and local cli png export paths · community library vector icon merge via script · render verification and user review iteration lo · routing guidance to drawio, mermaid, and tldraw
- Use cases
- documentation · ui design · planning
What excalidraw says it does
A box around every label makes a diagram look like a wireframe.
Rule: Do not invent new colors.
PDF is NOT supported.
npx skills add https://github.com/agents365-ai/365-skills --skill excalidrawAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3.1k |
|---|---|
| repo stars | ★ 29 |
| Security audit | 2 / 3 scanners passed |
| Last updated | August 2, 2026 |
| Repository | agents365-ai/365-skills ↗ |
How do I create a clean architecture or flowchart diagram and export it as PNG or SVG?
Generate Excalidraw diagrams and export PNG or SVG via Kroki API or local excalidraw-brute-export-cli.
Who is it for?
Explaining systems with 3+ components, flows, or relationships visually.
Skip if: Skip for strict UML, branded vendor polish, or mermaid-style diagrams-as-code; route to drawio or mermaid.
When should I use this skill?
User requests diagram, flowchart, architecture chart, visualize, or proactive 3+ component explanations.
What you get
Verified .excalidraw source plus exported PNG or SVG at an approved quality bar.
- Excalidraw version-2 JSON file
- architecture diagram
- user flow diagram
By the numbers
- Targets Excalidraw schema version 2
- Uses section-namespaced element seeds prefixed 100xxx
Files
Excalidraw Diagrams
Overview
Generate .excalidraw JSON files and export to PNG/SVG.
Two export options:
- Kroki API (
curl) — zero install, SVG output only - excalidraw-brute-export-cli — local Firefox-based, PNG + SVG
Supported formats: PNG (local CLI only), SVG (both options). PDF is NOT supported.
When to Use
Explicit triggers: user says "画图", "diagram", "visualize", "flowchart", "draw", "架构图", "流程图"
Proactive triggers:
- Explaining a system with 3+ interacting components
- Describing a multi-step process or decision tree
- Comparing architectures or approaches side by side
Skip when: a simple list or table suffices, or user is in a quick Q&A flow
When NOT to use it — route elsewhere:
- Polished, precise diagrams, strict UML, or branded vendor icons → drawio.
- Diagrams-as-code in git, auto-laid-out from text → mermaid (general) or plantuml (UML).
- An infinite-canvas whiteboard or programmatic freehand strokes → tldraw.
Prerequisites
Option A: Kroki API (recommended — zero install, SVG only)
# Just needs curl (pre-installed on macOS/Linux/Windows Git Bash)
curl --versionNo additional setup. SVG rendered via https://kroki.io.
Option B: Local CLI (required for PNG)
The CLI uses Firefox (not Chromium). Check and install:
npm install -g excalidraw-brute-export-cli
npx playwright install firefoxmacOS patch (one-time, required):
CLI_MAIN=$(npm root -g)/excalidraw-brute-export-cli/src/main.js
sed -i '' 's/keyboard.press("Control+O")/keyboard.press("Meta+O")/' "$CLI_MAIN"
sed -i '' 's/keyboard.press("Control+Shift+E")/keyboard.press("Meta+Shift+E")/' "$CLI_MAIN"Windows/Linux: No patch needed.
Workflow
1. Check deps — use Kroki (curl) for SVG; use local CLI for PNG 2. Plan — pick the visual metaphor (see Relationship-to-layout map), then the diagram type and color palette 3. Generate — write .excalidraw JSON file (section-by-section for large diagrams) 4. Export — run Kroki or CLI command 5. Verify the render — view the exported PNG, fix any defects, re-export (see Verify the Render) 6. Review loop — show the image to the user, apply the minimal .excalidraw edit per request, re-export until approved (see Review Loop) 7. Report — tell user the output file path
Design Principles
Default style
roughness: 0— clean, modern look for all technical diagrams (use1only when user requests hand-drawn/casual style)fontFamily: 2(Helvetica) — professional look; use1(Virgil) only for casual/sketch style,3(Cascadia) for code snippetsfillStyle: "solid"— default fill
Containers: prefer typography over boxes
A box around every label makes a diagram look like a wireframe. The cleanest Excalidraw diagrams use free-floating text and lines for structure and reserve filled boxes for things that are genuinely components.
- Default to no container — use a standalone
textelement unless the box earns its place. - Add a box only when the element is a real system component, an arrow binds to it, the shape itself carries meaning (decision diamond, start/end ellipse), or it groups a zone.
- Aim for under ~30% of text elements inside boxes.
- For timelines, trees, and hierarchies, use a line/connector + free-floating labels, not a stack of rectangles. Size, weight, and color create hierarchy without boxes.
Font size hierarchy
| Level | Size | Use for |
|---|---|---|
| Title | 28px | Diagram title |
| Header | 24px | Section/group headers |
| Label | 20px | Primary element labels |
| Description | 16px | Secondary text, descriptions |
| Note | 14px | Annotations, fine print |
Color palette
Follow the 60-30-10 rule: 60% whitespace/neutral, 30% primary accent, 10% highlight.
Semantic fill colors (use with strokeColor one shade darker):
| Category | Fill | Stroke | Use for |
|---|---|---|---|
| Primary / Input | #dbeafe | #1e40af | Entry points, APIs, user-facing |
| Success / Data | #dcfce7 | #166534 | Data stores, success states |
| Warning / Decision | #fef9c3 | #854d0e | Decision points, conditions |
| Error / Critical | #fee2e2 | #991b1b | Errors, alerts, critical paths |
| External / Storage | #f3e8ff | #6b21a8 | External services, databases, AI/ML |
| Process / Default | #e0f2fe | #0369a1 | Standard process steps |
| Trigger / Start | #fed7aa | #c2410c | Start nodes, triggers, events |
| Neutral / Container | #f1f5f9 | #475569 | Groups, swimlanes, backgrounds |
Text colors:
| Level | Color |
|---|---|
| Title | #1e293b |
| Label | #334155 |
| Description | #64748b |
Rule: Do not invent new colors. Pick from this palette.
Arrow semantics
| Style | Meaning |
|---|---|
Solid (strokeStyle: "solid") | Primary flow, main path |
Dashed ("dashed") | Response, async, callback |
Dotted ("dotted") | Optional, reference, weak dependency |
Excalidraw JSON Structure
File skeleton
{
"type": "excalidraw",
"version": 2,
"source": "claude-code",
"elements": [],
"appState": { "viewBackgroundColor": "#ffffff" }
}Element types
| type | use for |
|---|---|
| rectangle | boxes, components, modules |
| ellipse | start/end nodes, databases |
| diamond | decision points |
| arrow | directed connections |
| line | undirected connections |
| text | standalone labels |
image, frame, and embeddable are not covered by this skill: image needs a separate files map plus a fileId, and frames/embeds render inconsistently through the export path. Stick to the six types above — and for ready-made icons built from these primitives, see Community shape & icon libraries below.
Community shape & icon libraries
Need a real AWS / Azure / GCP / network / UML / BPMN icon instead of a plain box? The Excalidraw community libraries (200+ .excalidrawlib files) are built almost entirely from the same vector primitives above — so their items render through Kroki and the local CLI with no image element and no files map. Use the helper in scripts/excalidraw_lib.py:
# 1. Find a library (matches name / description / item names)
python scripts/excalidraw_lib.py search aws
# 2. List its items (index, name, element count; flags any image-based item)
python scripts/excalidraw_lib.py items slobodan/aws-serverless.excalidrawlib
# 3. Build your base scene first, then drop an item in at (x, y). IDs are
# namespaced and coordinates translated, so it merges without collisions:
python scripts/excalidraw_lib.py merge scene.excalidraw \
slobodan/aws-serverless.excalidrawlib 0 455 257 --scale 0.9 --prefix lambdaRules:
- Vector only.
mergerefuses any item containing animageelement (won't render via the export path);itemsflags them up front. - Use sparingly. An icon is just a labeled node — keep the design system's spacing, labels, and arrow semantics. Icons accent a diagram; they don't replace it.
- Arrows don't bind to library groups — draw connectors with explicit edge-to-edge
points(bindings don't affect the static export anyway). - Libraries are MIT-licensed; a courtesy credit is welcome, not required.
- Still run Verify the Render afterward — icon bounding boxes vary, so check alignment and spacing.
Element sizing
Calculate element width from label text to prevent truncation:
Latin text: width = max(160, charCount * 9)
CJK text: width = max(160, charCount * 18)
Mixed text: estimate each character individually, sum upHeight: use 60 for single-line labels, add 24 per additional line.
Standalone `text` does NOT auto-wrap. For multi-line standalone labels, insert manual \n line breaks yourself — aim for ≤ ~30 Latin (≤ ~15 CJK) characters per line at 16px — and add 24 height per line. (Text bound inside a shape via containerId wraps to the container width automatically, so size the container instead of adding \n.)
Required properties (all elements)
{
"id": "auth_service",
"type": "rectangle",
"x": 100, "y": 100,
"width": 160, "height": 60,
"angle": 0,
"strokeColor": "#1e40af",
"backgroundColor": "#dbeafe",
"fillStyle": "solid",
"strokeWidth": 2,
"roughness": 0,
"opacity": 100,
"seed": 100001,
"boundElements": [
{ "id": "arrow_to_db", "type": "arrow" },
{ "id": "label_auth", "type": "text" }
]
}Use descriptive string IDs (e.g., "api_gateway", "arrow_gw_to_auth") instead of random strings.
Give each element a unique seed (integer). Namespace by section: 100xxx, 200xxx, 300xxx.
JSON field rules
boundElements: usenullwhen empty, never[]updated: always use1, never timestamps- Do NOT include:
frameId,index,versionNonce,rawText pointsin arrows: always start at[0, 0]seed: must be a positive integer, unique per element
Property values
Use only these values — all verified to render via Kroki and the local CLI:
| Property | Valid values |
|---|---|
fillStyle | "solid", "hachure", "cross-hatch", "zigzag" |
strokeStyle | "solid" (or omit), "dashed", "dotted" |
fontFamily | 1 (Virgil, hand-drawn), 2 (Helvetica), 3 (Cascadia, code) |
textAlign | "left", "center", "right" |
verticalAlign | "top", "middle", "bottom" |
startArrowhead / endArrowhead | null, "arrow", "triangle", "bar", "dot", "circle", "diamond", "crowfoot_many" |
Arrows default to endArrowhead: "arrow" and startArrowhead: null — omit both for a standard one-way arrow. Use "triangle" for UML inheritance, "diamond" for composition, and "crowfoot_many" for ER cardinality.
Need copy-paste templates or the full property/arrowhead catalogue? Read references/schema-reference.md — complete element templates (component+label, bound arrow, arrow label, swimlane zone, mind-map connector) and every verified property value.Text inside shapes (contained text)
When text belongs inside a shape, bind them bidirectionally:
{
"id": "label_auth",
"type": "text",
"text": "Auth Service",
"fontSize": 20,
"fontFamily": 2,
"textAlign": "center",
"verticalAlign": "middle",
"strokeColor": "#1e293b",
"containerId": "auth_service"
}CRITICAL: Text `strokeColor` is the text color. Always set it explicitly to a dark color from the text color palette. Never omit it — omitting strokeColor on text can cause invisible text that blends with the shape background.
The parent shape must list the text in its boundElements:
"boundElements": [{ "id": "label_auth", "type": "text" }]Arrow binding (bidirectional)
Arrows must bind to shapes, and shapes must reference bound arrows:
{
"id": "arrow_gw_to_auth",
"type": "arrow",
"points": [[0, 0], [200, 0]],
"startBinding": { "elementId": "api_gateway", "gap": 5, "focus": 0 },
"endBinding": { "elementId": "auth_service", "gap": 5, "focus": 0 }
}Both api_gateway and auth_service must include in their boundElements:
"boundElements": [{ "id": "arrow_gw_to_auth", "type": "arrow" }]Endpoints must reach the shape borders. startBinding/endBinding (and their gap) only affect interactive editing on excalidraw.com — they do NOT clip the line when exporting via Kroki or the local CLI. The exporter draws your points literally. So compute endpoints edge-to-edge: set the arrow's x/y to the source shape's border (the side facing the target) and the last point to the target's border. Center-to-center points draw the line straight through both shapes.
Arrow labels
To label an arrow, bind a text element to it exactly like shape text: set the label's containerId to the arrow's id, and add the label to the arrow's boundElements. Excalidraw then centers the label on the arrow and masks the line behind the text, so it stays readable (no strike-through).
{
"id": "arrow_valid_to_grant",
"type": "arrow",
"points": [[0, 0], [0, 120]],
"boundElements": [{ "id": "lbl_yes", "type": "text" }]
}{
"id": "lbl_yes",
"type": "text",
"text": "Yes",
"fontSize": 14,
"width": 36,
"strokeColor": "#1e293b",
"containerId": "arrow_valid_to_grant"
}*CRITICAL: the label `width` must fit the text (`charCount 9`), NOT the arrow length.* Excalidraw masks the line behind the label's full bounding box — a label as wide as the arrow masks the entire* arrow, so the line disappears and only floating text remains. Keep label widths small.
Arrow routing
L-shaped (elbow) arrows — orthogonal routing with 3+ points:
"points": [[0, 0], [100, 0], [100, 150]]Elbowed arrows — automatic right-angle routing:
{
"type": "arrow",
"points": [[0, 0], [0, -50], [200, -50], [200, 0]],
"elbowed": true
}Curved arrows — smooth routing with waypoints:
{
"type": "arrow",
"points": [[0, 0], [50, -40], [200, 0]],
"roundness": { "type": 2 }
}Grouping
Related elements share groupIds. Nested groups list IDs innermost-first:
"groupIds": ["inner_group", "outer_group"]Diagram Patterns
Choose the right visual pattern for each diagram type.
Relationship-to-layout map
Before locking in a diagram type, pick the visual metaphor that matches the relationship in the idea — it drives the layout more than the type label does:
| Relationship in the idea | Visual metaphor | Build with |
|---|---|---|
| One → many (broadcast, dispatch) | Fan-out | one node, arrows radiating outward |
| Many → one (aggregate, merge) | Convergence | several inputs, arrows into one node |
| Parent → children (hierarchy) | Tree | trunk + branch lines, free-floating text |
| Repeating cycle (loop, feedback) | Cycle | nodes in a ring, curved arrows back to start |
| Input → transform → output | Assembly line | left-to-right pipeline of steps |
| A vs B (comparison) | Side-by-side | two parallel columns on a shared baseline |
| Before / after, phase break | Gap | whitespace or a dashed divider between groups |
| Fuzzy / overlapping state | Cloud | overlapping ellipses, no hard borders |
Spacing Reference
| Scenario | Spacing |
|---|---|
| Labeled arrow gap (between shapes) | 150–200px |
| Unlabeled arrow gap | 100–120px |
| Column spacing (labeled arrows) | 400px (220px box + 180px gap) |
| Column spacing (unlabeled arrows) | 340px (220px box + 120px gap) |
| Row spacing | 280–350px (150px box + 130–200px gap) |
| Zone/container padding | 50–60px around children |
| Zone/container opacity | 25–40 |
| Minimum gap between any elements | 40px |
Flowchart (LR or TB)
- Ellipse for start/end, diamond for decisions, rectangle for process
- 200px horizontal spacing, 150px vertical spacing
- Decision branches: "Yes" goes forward, "No" goes down
- 3–10 steps (max 15)
Architecture / System Diagram
- Column spacing per table above; use labeled arrow spacing when connections have labels
- Group related services in dashed
Neutralcontainers (opacity: 30, padding: 50px) - Gateway/entry at left or top, databases at right or bottom
- 3–8 entities (max 12)
Sequence Diagram
- 200px between participants (rectangles at top)
- Vertical lifelines as dashed lines
- Horizontal arrows for messages, 60px vertical spacing
- Solid arrow = request, dashed arrow = response
Mind Map
- Central node: largest (200x100),
Triggercolor - Level 1: 150x70,
Primarycolor, radial around center - Level 2: 120x50,
Processcolor - Level 3: 90x40,
Neutralcolor - Use lines (not arrows) for connections
- 4–6 branches (max 8), 2–4 sub-topics per branch
- Place level-1 branches on a circle of radius
R ≈ 280around the center(cx, cy): for branchiofn,angle = 2π·i/n,x = cx + R·cos(angle),y = cy + R·sin(angle). Even spacing prevents the crossed-line tangle that ad-hoc placement produces.
Swimlane
- Large transparent rectangles (
Neutralfill,"dashed"stroke, opacity: 30) as lane boundaries - Lane label as free-standing text at top-left of lane (not bound to rectangle), 28px font
- Elements flow left-to-right within lanes
- Arrows cross lanes for handoffs
Section-by-Section Construction
For diagrams with 10+ elements, do NOT generate the entire JSON at once. Build in sections:
1. Plan all sections first — list element IDs, positions, and cross-section bindings 2. Write section 1 — create the file with initial elements 3. Append section 2 — read the file, add new elements to the elements array 4. Repeat — continue until all sections are done 5. Final pass — verify all boundElements and startBinding/endBinding references are consistent
Namespace element seeds by section (100xxx, 200xxx, 300xxx) to avoid collisions.
Export
Option A: Kroki API (SVG only — zero install)
# SVG via Kroki API
curl -s -X POST https://kroki.io/excalidraw/svg \
-H "Content-Type: text/plain" \
--data-binary "@diagram.excalidraw" \
-o diagram.svg
# Via local Kroki Docker (offline)
curl -s -X POST http://localhost:8000/excalidraw/svg \
-H "Content-Type: text/plain" \
--data-binary "@diagram.excalidraw" \
-o diagram.svgOption B: Local CLI (PNG + SVG)
# PNG at 2x scale, with background baked in (recommended)
excalidraw-brute-export-cli -i diagram.excalidraw -o diagram.png -f png -s 2 -b true
# PNG at 1x scale
excalidraw-brute-export-cli -i diagram.excalidraw -o diagram.png -f png -s 1 -b true
# SVG
excalidraw-brute-export-cli -i diagram.excalidraw -o diagram.svg -f svg -s 1 -b trueRequired flags: -f (format: png or svg) and -s (scale: 1, 2, or 3).
Optional flags: -b true bakes the viewBackgroundColor into the image — the export is transparent by default, so omit -b (or pass -b false) only when you want a transparent background. -d true exports dark mode; -e true embeds the scene so the PNG/SVG reopens as an editable drawing in excalidraw.com. (Long forms also work: --background, --dark-mode, --embed-scene, --format, --scale, --input, --output.)
Verify the Render
You cannot judge a diagram from its JSON. The JSON can look perfect while the image has clipped text, overlapping boxes, or an arrow slicing through a shape. After exporting, look at the result and fix it — this is the single highest-leverage step.
1. Render to PNG (the image must be viewable — PNG, not SVG, even if the user ultimately wants SVG):
excalidraw-brute-export-cli -i diagram.excalidraw -o /tmp/check.png -f png -s 2 -b trueView /tmp/check.png (Claude can read PNGs directly). Visual audit needs the local CLI; with Kroki-only (SVG), fall back to the structural checks below. 2. Audit the image:
| Look for | Fix |
|---|---|
| Text clipped / overflowing its shape | Widen the shape (max(160, charCount * 9), ×2 for CJK) or pre-wrap with \n |
| Boxes or labels overlapping | Re-space using the Spacing Reference (≥40px gap) |
| Arrow cutting straight through a shape | Move endpoints to the shape borders, not centers |
| Arrow invisible — only its label shows | Shrink the label width to fit the text |
| Element off-canvas or floating with no connection | Reposition / connect it |
| Isomorphism Test: mentally delete all text — does the structure alone still convey the idea? | If not, the layout is wrong, not the labels — restructure |
3. Fix the JSON and re-export. Repeat until clean — typically 1–3 passes. Skip only for trivial 2–3 element diagrams.
Review Loop
Verify-the-render fixes defects; the review loop incorporates the user's wishes. After the render is clean, show it and collect feedback, then apply the minimal `.excalidraw` edit for each request and re-export:
| User request | Edit action |
|---|---|
| Change a label | Edit the text (or the bound label element) |
| Change a color | Update backgroundColor / strokeColor on the element |
| Add / remove an element | Append or delete the element (fix any boundElements / binding refs) |
| Move / resize | Update x / y / width / height |
| Restructure / re-route | Re-apply the pattern's spacing & routing rules, or regenerate |
- Overwrite the same
diagram.excalidraw/ output file each round — don't createv1,v2, … - Re-run Verify the Render after each edit (a change can introduce a new clip / overlap).
- Safety valve: after 5 rounds, suggest the user fine-tune in excalidraw.com — the output preserves arrow binding, so it opens fully editable.
Anti-Patterns
Never put `text` on large background/zone rectangles. Excalidraw centers text in the middle of the shape, overlapping contained elements. Instead, use a free-standing text element positioned at the top of the zone.
Avoid cross-zone arrows. Long diagonal arrows create visual spaghetti. Route arrows within zones or along zone edges. If a cross-zone connection is unavoidable, route it along the perimeter.
Use arrow labels sparingly. Bind labels to the arrow (see Arrow labels) so the line is masked behind the text instead of striking through it — but keep the label width to the text, never the arrow length. Keep labels to ≤12 characters and ensure ≥120px clear space between connected shapes. Omit labels when the connection meaning is obvious from context.
Don't use filled backgrounds on containers that hold other elements. Use opacity: 30 (or 25-40 range) for zone/container rectangles so contained elements remain visible.
Always set explicit `strokeColor` on text elements. Text strokeColor is the rendered text color. If omitted, text may inherit the parent shape's background color and become invisible. Use #1e293b (title), #334155 (label), or #64748b (description) from the text color palette.
Common Mistakes
| Mistake | Fix |
|---|---|
| Kroki returns HTTP 400 | Send -H "Content-Type: text/plain" (NOT application/json, which Kroki reads as a {"diagram_source": ...} wrapper and rejects); ensure valid JSON with "type": "excalidraw" and "elements" array |
| Kroki only outputs SVG | Use local CLI (excalidraw-brute-export-cli) for PNG |
| Export fails with "Missing required flag" | Always pass -f png and -s 2 |
| Export fails with "Executable doesn't exist" | Run npx playwright install firefox |
| macOS: timeout waiting for file chooser | Apply the macOS Meta patch above |
Arrow points not relative to origin | points always start at [0,0] |
Missing id on elements | Use descriptive string IDs per element |
| Overlapping elements | Use spacing reference table; minimum 40px gap |
| Arrows not interactive in excalidraw.com | Add boundElements to shapes referencing all bound arrows/text |
| Arrow/line cuts straight through the shapes | Compute endpoints at the shape borders, not centers — bindings don't clip the static export |
| Arrow invisible — only its label shows | Bound label width spans the whole arrow and masks the line; set label width to fit the text (charCount * 9) |
| Exported PNG/SVG has no background | CLI export is transparent by default; pass -b true to bake in viewBackgroundColor |
| Text not centered in shape | Set containerId on text AND add text to shape's boundElements |
| All text same size | Use font size hierarchy: 28 → 24 → 20 → 16 → 14 |
| Diagram looks monotone | Apply semantic colors from the palette, follow 60-30-10 rule |
| Text invisible / same color as background | Always set strokeColor on text elements to a dark color (#1e293b, #334155, or #64748b) |
| Text overlaps inside zone/container | Don't bind text to zone rectangles; use free-standing text at top |
| Text truncated in shapes | Use width formula: max(160, charCount * 9), double for CJK |
boundElements: [] causes issues | Use null for empty boundElements, never [] |
Excalidraw Schema Reference
On-demand depth for the excalidraw skill. Read this when you need a copy-paste starting point or the full catalogue of a property's values. The core generate-time rules (binding, endpoints, sizing, arrow labels) live in SKILL.md — this file is the long tail, not a replacement.
Copy-paste element templates
All templates use descriptive string IDs and section-namespaced seeds (100xxx). Swap colors from the palette in SKILL.md.
File skeleton
{
"type": "excalidraw",
"version": 2,
"source": "claude-code",
"elements": [],
"appState": { "viewBackgroundColor": "#ffffff" }
}Rectangle component + bound label (the workhorse)
{
"id": "auth_service", "type": "rectangle",
"x": 100, "y": 100, "width": 160, "height": 60, "angle": 0,
"strokeColor": "#1e40af", "backgroundColor": "#dbeafe",
"fillStyle": "solid", "strokeWidth": 2, "roughness": 0, "opacity": 100,
"seed": 100001,
"boundElements": [{ "id": "label_auth", "type": "text" }]
},
{
"id": "label_auth", "type": "text", "text": "Auth Service",
"fontSize": 20, "fontFamily": 2, "textAlign": "center", "verticalAlign": "middle",
"strokeColor": "#1e293b", "seed": 100002,
"containerId": "auth_service"
}Bound arrow between two shapes
Both shapes must also list this arrow id in their own boundElements.
{
"id": "arrow_gw_to_auth", "type": "arrow",
"x": 260, "y": 130, "width": 200, "height": 0, "angle": 0,
"strokeColor": "#475569", "backgroundColor": "transparent",
"fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid",
"roughness": 0, "opacity": 100, "seed": 100010,
"points": [[0, 0], [200, 0]],
"startBinding": { "elementId": "api_gateway", "gap": 5, "focus": 0 },
"endBinding": { "elementId": "auth_service", "gap": 5, "focus": 0 }
}Arrow with a bound label (Yes/No, message names)
Label width must fit the text, never the arrow — a full-width label masks the whole line.
{
"id": "arrow_valid_yes", "type": "arrow",
"x": 200, "y": 300, "width": 0, "height": 120,
"points": [[0, 0], [0, 120]], "seed": 100020,
"strokeColor": "#475569", "strokeWidth": 2, "roughness": 0, "opacity": 100,
"boundElements": [{ "id": "lbl_yes", "type": "text" }]
},
{
"id": "lbl_yes", "type": "text", "text": "Yes",
"fontSize": 14, "width": 36, "fontFamily": 2,
"strokeColor": "#1e293b", "seed": 100021,
"containerId": "arrow_valid_yes"
}Start/end ellipse and decision diamond
{ "id": "start", "type": "ellipse", "x": 60, "y": 40, "width": 140, "height": 70,
"strokeColor": "#c2410c", "backgroundColor": "#fed7aa", "fillStyle": "solid",
"strokeWidth": 2, "roughness": 0, "opacity": 100, "seed": 100030,
"boundElements": [{ "id": "lbl_start", "type": "text" }] },
{ "id": "decide", "type": "diamond", "x": 60, "y": 260, "width": 160, "height": 100,
"strokeColor": "#854d0e", "backgroundColor": "#fef9c3", "fillStyle": "solid",
"strokeWidth": 2, "roughness": 0, "opacity": 100, "seed": 100031,
"boundElements": [{ "id": "lbl_decide", "type": "text" }] }Swimlane zone + free-standing lane label
Zone text is a separate top-left text element, never bound to the rectangle.
{ "id": "lane_customer", "type": "rectangle", "x": 40, "y": 40,
"width": 1000, "height": 200, "strokeColor": "#475569",
"backgroundColor": "#f1f5f9", "fillStyle": "solid", "strokeStyle": "dashed",
"strokeWidth": 2, "roughness": 0, "opacity": 30, "seed": 100040,
"boundElements": null },
{ "id": "lbl_lane_customer", "type": "text", "text": "Customer",
"x": 56, "y": 52, "fontSize": 28, "fontFamily": 2,
"strokeColor": "#334155", "seed": 100041, "containerId": null }Mind-map line connector (not an arrow)
{ "id": "line_center_a", "type": "line",
"x": 500, "y": 300, "width": 180, "height": -120,
"points": [[0, 0], [180, -120]],
"strokeColor": "#475569", "strokeWidth": 2, "roughness": 0,
"opacity": 100, "seed": 100050 }Full property catalogue
Every value below is verified to render through Kroki and the local CLI.
| Property | Valid values | Notes |
|---|---|---|
fillStyle | "solid", "hachure", "cross-hatch", "zigzag" | solid for clean diagrams; the others are sketch textures |
strokeStyle | "solid" (or omit), "dashed", "dotted" | dashed = response/async, dotted = optional/weak |
fontFamily | 1 (Virgil, hand-drawn), 2 (Helvetica), 3 (Cascadia, code) | 2 for technical diagrams |
textAlign | "left", "center", "right" | center for contained labels |
verticalAlign | "top", "middle", "bottom" | middle for contained labels |
roughness | 0 (clean), 1 (hand-drawn), 2 (very sketchy) | default 0 |
strokeWidth | 1 (thin), 2 (standard), 3 (bold) | use width for emphasis, not color noise |
roundness | null (sharp), { "type": 2 } (rounded/curved) | type: 2 on arrows = curved routing |
Arrowhead catalogue (startArrowhead / endArrowhead)
| Value | Use for |
|---|---|
null | no head (lines, mind-map connectors, startArrowhead of a one-way arrow) |
"arrow" | standard directed flow (default endArrowhead) |
"triangle" | UML inheritance / generalization |
"diamond" | UML composition |
"bar" | UML aggregation, hard stop |
"dot" / "circle" | endpoint markers, BPMN-ish |
"crowfoot_many" | ER "many" cardinality |
Arrows default to endArrowhead: "arrow", startArrowhead: null — omit both for a standard one-way arrow.
JSON field rules (recap)
boundElements: usenullwhen empty, never[].updated: always1, never timestamps.- Do NOT include:
frameId,index,versionNonce,rawText. - Arrow
pointsalways start at[0, 0]; the arrow'sx/yposition the first point. seed: positive integer, unique per element, namespaced by section (100xxx, 200xxx…).- Unsupported element types:
image(needs afilesmap +fileId),frame,embeddable.
#!/usr/bin/env python3
"""Fetch and embed Excalidraw community-library items into a .excalidraw scene.
Community libraries (https://libraries.excalidraw.com, repo excalidraw/excalidraw-libraries)
are almost all built from the same vector primitives this skill already exports
(rectangle/ellipse/line/arrow/diamond/text/freedraw), so their items render correctly
through Kroki and the local brute-export CLI. This helper discovers libraries, lists
their items, and merges an item into a scene with namespaced IDs and translated
coordinates so it drops in without collisions.
Usage:
excalidraw_lib.py search <keyword>
List libraries whose name/description/items match <keyword>.
excalidraw_lib.py items <source>
List the items in a library (index, name, element count, image flag).
excalidraw_lib.py merge <scene.excalidraw> <source> <item> <x> <y> [--prefix P] [--scale S]
Add <item> (index or name substring) to the scene at (x, y), in place.
<source> is "author/name.excalidrawlib" (fetched + cached from the official repo)
or a path to a local .excalidrawlib file.
"""
import argparse, copy, json, math, os, sys, urllib.request
BASE = "https://raw.githubusercontent.com/excalidraw/excalidraw-libraries/main"
CACHE = "/tmp/excalidraw-libs"
def fetch(url, dest):
os.makedirs(os.path.dirname(dest), exist_ok=True)
if not os.path.exists(dest):
urllib.request.urlretrieve(url, dest)
return dest
def load_index():
return json.load(open(fetch(f"{BASE}/libraries.json", f"{CACHE}/libraries.json")))
def load_lib(source):
if os.path.exists(source):
return json.load(open(source))
path = fetch(f"{BASE}/libraries/{source}", f"{CACHE}/{source}")
return json.load(open(path))
def items_of(lib):
"""Return list of (name, elements) regardless of library format version."""
raw = lib.get("libraryItems") or lib.get("library") or []
out = []
for i, it in enumerate(raw):
if isinstance(it, list):
out.append((f"item{i}", it))
else:
out.append((it.get("name") or f"item{i}", it.get("elements", [])))
return out
def _elem_bounds(e):
"""True visual bounds of one element — accounts for `points` (lines/arrows)
and `angle` (rotation), both of which a naive x/y/w/h box misses and which
otherwise mis-place rotated or arrow-based library icons."""
x, y, w, h = e.get("x", 0), e.get("y", 0), e.get("width", 0), e.get("height", 0)
pts = e.get("points")
if pts:
xs = [x + p[0] for p in pts]; ys = [y + p[1] for p in pts]
x0, y0, x1, y1 = min(xs), min(ys), max(xs), max(ys)
else:
x0, y0, x1, y1 = x, y, x + w, y + h
a = e.get("angle", 0) or 0
if a:
cx, cy = (x0 + x1) / 2, (y0 + y1) / 2
ca, sa = math.cos(a), math.sin(a)
corners = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]
rx = [cx + (px - cx) * ca - (py - cy) * sa for px, py in corners]
ry = [cy + (px - cx) * sa + (py - cy) * ca for px, py in corners]
x0, x1, y0, y1 = min(rx), max(rx), min(ry), max(ry)
return x0, y0, x1, y1
def bbox(els):
b = [_elem_bounds(e) for e in els]
return min(p[0] for p in b), min(p[1] for p in b), max(p[2] for p in b), max(p[3] for p in b)
def place(els, tx, ty, prefix, scale):
x0, y0, _, _ = bbox(els)
idmap = {e["id"]: f"{prefix}_{e['id']}" for e in els if "id" in e}
out = []
for e in els:
e = copy.deepcopy(e)
if "id" in e:
e["id"] = idmap[e["id"]]
e["x"] = (e.get("x", 0) - x0) * scale + tx
e["y"] = (e.get("y", 0) - y0) * scale + ty
for k in ("width", "height"):
if k in e:
e[k] *= scale
if "points" in e:
e["points"] = [[px * scale, py * scale] for px, py in e["points"]]
if "fontSize" in e:
e["fontSize"] *= scale
if e.get("containerId") in idmap:
e["containerId"] = idmap[e["containerId"]]
for be in (e.get("boundElements") or []):
if isinstance(be, dict) and be.get("id") in idmap:
be["id"] = idmap[be["id"]]
for b in ("startBinding", "endBinding"):
if e.get(b) and e[b].get("elementId") in idmap:
e[b]["elementId"] = idmap[e[b]["elementId"]]
out.append(e)
return out
def resolve_item(items, sel):
if sel.isdigit():
return items[int(sel)]
for name, els in items:
if sel.lower() in name.lower():
return name, els
sys.exit(f"No item matching '{sel}'. Run: excalidraw_lib.py items <source>")
def cmd_search(args):
hits = 0
for lib in load_index():
names = " ".join(lib.get("itemNames", []))
hay = f"{lib['name']} {lib.get('description','')} {names}".lower()
if args.keyword.lower() in hay:
hits += 1
print(f"{lib['source']}\n {lib['name']} — {len(lib.get('itemNames', []))} items")
if not hits:
print(f"No libraries match '{args.keyword}'. Browse: https://libraries.excalidraw.com")
def cmd_items(args):
for i, (name, els) in enumerate(items_of(load_lib(args.source))):
img = " [HAS IMAGE — won't render via export]" if any(e.get("type") == "image" for e in els) else ""
print(f"{i:3} {name} ({len(els)} els){img}")
def cmd_merge(args):
name, els = resolve_item(items_of(load_lib(args.source)), args.item)
if any(e.get("type") == "image" for e in els):
sys.exit(f"Item '{name}' contains an image element — skip it (won't render via Kroki/CLI).")
scene = json.load(open(args.scene))
prefix = args.prefix or f"lib{len(scene['elements'])}"
scene["elements"].extend(place(els, args.x, args.y, prefix, args.scale))
json.dump(scene, open(args.scene, "w"))
print(f"Merged '{name}' ({len(els)} els) at ({args.x},{args.y}) into {args.scene}")
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
sub = ap.add_subparsers(dest="cmd", required=True)
s = sub.add_parser("search"); s.add_argument("keyword"); s.set_defaults(fn=cmd_search)
s = sub.add_parser("items"); s.add_argument("source"); s.set_defaults(fn=cmd_items)
s = sub.add_parser("merge")
s.add_argument("scene"); s.add_argument("source"); s.add_argument("item")
s.add_argument("x", type=float); s.add_argument("y", type=float)
s.add_argument("--prefix"); s.add_argument("--scale", type=float, default=1.0)
s.set_defaults(fn=cmd_merge)
args = ap.parse_args()
args.fn(args)
if __name__ == "__main__":
main()
Related skills
Forks & variants (1)
Excalidraw has 1 known copy in the catalog totaling 74 installs. They canonicalize to this original listing.
- agents365-ai - 74 installs
How it compares
Choose excalidraw when you need editable whiteboard JSON for Excalidraw rather than static PNG exports or text-based diagram languages.
FAQ
How do I get PNG versus SVG?
Kroki via curl exports SVG only; install excalidraw-brute-export-cli with Firefox for PNG.
Should every label sit in a box?
No. Use free-floating text; boxes only for real components or meaningful shapes.
Can I use AWS or cloud icons?
Yes via excalidraw_lib.py merge of vector community library items without image elements.
Is Excalidraw safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.