| name | excalidraw | ||||||
|---|---|---|---|---|---|---|---|
| description | Excalidraw diagram authoring — JSON format, element types, styling conventions, color palettes, and SVG/PNG generation via CLI. Use when creating or editing .excalidraw diagram files programmatically. | ||||||
| license | MIT | ||||||
| metadata |
|
Instructions for creating professional Excalidraw diagrams programmatically as JSON files and converting them to SVG/PNG.
Excalidraw files (.excalidraw) are JSON with this top-level structure:
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [ ... ],
"appState": {
"gridSize": null,
"viewBackgroundColor": "#ffffff"
},
"files": {}
}The elements array contains all shapes, text, and arrows. Order matters — later elements render on top.
Every element MUST have these properties:
{
"id": "unique-id",
"type": "rectangle",
"x": 100, "y": 100, "width": 200, "height": 50,
"strokeColor": "#1e1e1e",
"backgroundColor": "#e9ecef",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"angle": 0,
"groupIds": [],
"roundness": { "type": 3 },
"seed": 1001,
"version": 1,
"versionNonce": 1001,
"isDeleted": false,
"boundElements": null,
"updated": 1,
"link": null,
"locked": false
}Key fields:
id— unique string identifier. Use descriptive kebab-case names (e.g.,srv01-vector,arrow-stn-smb).seed— arbitrary integer, must be unique per element. Use incrementing numbers (1001, 1002, ...).versionNonce— set equal toseedfor new elements.roughness— always use0for clean, professional lines. Value1gives hand-drawn look.roundness—{ "type": 3 }for rounded rectangles,{ "type": 2 }for arrows/diamonds,nullfor text.boundElements— array of{ "id": "arrow-id", "type": "arrow" }for elements that arrows connect to. Set tonullif no arrows connect.
{
"type": "rectangle",
"x": 100, "y": 100, "width": 200, "height": 50,
"roundness": { "type": 3 }
}Use rectangles for: containers/groups, service boxes, callout banners.
{
"type": "diamond",
"x": 100, "y": 100, "width": 130, "height": 120,
"roundness": { "type": 2 }
}Use diamonds for: decision points, VIPs, load balancers, routing elements.
{
"type": "text",
"x": 110, "y": 110, "width": 180, "height": 25,
"text": "Label text here",
"fontSize": 14,
"fontFamily": 2,
"textAlign": "center",
"verticalAlign": "top",
"containerId": null,
"originalText": "Label text here",
"autoResize": true,
"lineHeight": 1.25,
"roundness": null
}Text is always a separate element positioned over its parent shape. It is NOT contained inside the shape — you must manually position it.
Multi-line text uses \n:
"text": "Line one\nLine two\nLine three",
"originalText": "Line one\nLine two\nLine three"{
"type": "arrow",
"x": 300, "y": 150, "width": 150, "height": 0,
"points": [[0, 0], [150, 0]],
"lastCommittedPoint": null,
"startBinding": { "elementId": "source-id", "focus": 0, "gap": 2 },
"endBinding": { "elementId": "target-id", "focus": 0, "gap": 2 },
"startArrowhead": null,
"endArrowhead": "arrow",
"roundness": { "type": 2 }
}Arrow-specific fields:
points— array of[dx, dy]offsets from the arrow'sx, yorigin. First point is always[0, 0].startBinding/endBinding— connect the arrow to elements. The target element MUST list this arrow in itsboundElementsarray. Set tonullfor unbound (floating) arrows.elementId— the ID of the element to bind to.focus— float from -1 to 1, controls where on the element edge the arrow attaches.0= center, negative = toward top/left, positive = toward bottom/right.gap— pixel gap between arrow tip and element edge. Use2.
startArrowhead—null(no arrowhead) or"arrow"(arrowhead at start).endArrowhead—"arrow"(standard arrowhead) ornull.- For bi-directional arrows: set both
startArrowhead: "arrow"andendArrowhead: "arrow".
Important: When an arrow binds to an element, you MUST also add the arrow to that element's boundElements array:
// On the target rectangle:
"boundElements": [
{ "id": "arrow-stn-vector", "type": "arrow" }
]Always use roughness: 0 for professional diagrams. This produces clean, sharp lines.
| Value | Effect |
|---|---|
0 |
Clean/sharp lines (professional) |
1 |
Slightly rough (default Excalidraw hand-drawn look) |
2 |
Very rough/sketchy |
The fontFamily integer maps to fonts in the rendering engine:
| Value | Font | Use for |
|---|---|---|
1 |
Virgil (hand-drawn) | Never use for professional diagrams |
2 |
Helvetica | All text labels, titles, descriptions |
3 |
Cascadia (monospace) | Code-like text: IPs, ports, paths, commands, protocols |
4 |
Excalifont | Avoid — renders as Virgil in excalirender |
Rule: Use fontFamily: 2 (Helvetica) for everything except code/technical values which use fontFamily: 3 (Cascadia).
| Role | fontSize | Example |
|---|---|---|
| Group/server title | 18 |
"srv01 (master)", "AWS Cloud" |
| Section title | 16 |
"Windows Stations (50+)" |
| Service box label | 14 |
"Vector Aggregator", "Samba Server" |
| Detail text / sublabel | 13 |
"Jenkins Agent", "Keepalived" |
| Description / muted text | 12 |
"Each station runs:", "→ Loki (logs)" |
| Arrow labels | 11 |
"mTLS gRPC", "HTTPS", "Vector :9000/9001" |
| Small annotations | 10 |
Compact arrow labels when space is tight |
| Role | strokeWidth | Usage |
|---|---|---|
| Group containers | 2 |
Outer boundary of logical groups (servers, cloud, stations) |
| Service boxes | 1 |
Individual service rectangles inside groups |
| Primary arrows | 2 |
Main data flow arrows |
| Secondary arrows | 1 |
Internal/standby/dashed arrows |
| Value | Usage |
|---|---|
"solid" |
Default for all active/primary elements |
"dashed" |
Standby/backup elements, inactive paths, internal flows |
| Value | Usage |
|---|---|
100 |
Default for all active elements |
60 |
Standby/secondary arrows and their labels |
Use a consistent semantic color scheme. Each color has a stroke (saturated) and background (pastel) pair.
| Semantic Role | strokeColor | backgroundColor | Usage |
|---|---|---|---|
| Neutral / container | #1e1e1e |
#e9ecef |
Generic groups, station containers |
| Active / healthy | #2f9e44 |
#ebfbee |
Active servers, healthy state |
| Error / danger | #e03131 |
#fff5f5 / #ffe3e3 |
Idle/failed servers, SPOF callouts, critical sync |
| Primary service (blue) | #1971c2 |
#d0ebff / #e7f5ff |
Samba, SMB, station items |
| Telemetry (orange) | #e8590c |
#fff4e6 |
Vector, monitoring, metrics |
| Storage (purple) | #6741d9 |
#f3f0ff |
rclone, S3, storage sync |
| Routing / VIP (amber) | #f08c00 |
#fff3bf |
Keepalived, VIP, load balancers |
| Utility / secondary | #868e96 |
#f8f9fa |
Jenkins, IAM, non-critical services |
| Cloud | #1971c2 |
#e7f5ff |
Cloud provider groups (AWS, GCP) |
| Role | strokeColor |
|---|---|
| Group title text | Same as group's strokeColor |
| Service label text | Same as service box's strokeColor |
| Muted/description text | #868e96 |
| Arrow label text | Same as arrow's strokeColor |
Text elements should always have "backgroundColor": "transparent".
| Between | Gap (px) |
|---|---|
| Group container edge → inner elements | 20-25 |
| Inner elements (vertical stacking) | 8-14 |
| Major groups (horizontal) | 60-100 |
| Major groups (vertical) | 40-60 |
| Arrow label → arrow line | 8-12 above the line |
- Group titles: Centered horizontally within the group, 10-12px from top edge.
- Service boxes: Left-aligned within group with equal left/right padding (20-25px).
- Service labels: Centered within their service box.
- Arrow labels: Positioned above or beside the arrow, using the same color as the arrow.
- Typical diagram width: 1000-1200px
- Typical diagram height: 500-650px
- Leave 30-40px margin on all sides from the outermost elements
Use descriptive kebab-case IDs:
- Groups:
srv01-group,cloud-group,stations-group - Labels:
srv01-label,cloud-label - Services:
srv01-vector,srv01-samba - Service labels:
srv01-vector-label - Arrows:
arrow-stn-vector,arrow-srv01-cloud - Arrow labels:
arrow-stn-vector-label
A server or logical group containing multiple services:
┌─ Group container (strokeWidth: 2, semantic color) ──────┐
│ Title (fontSize: 18, same strokeColor as group) │
│ Subtitle/IP (fontSize: 12, fontFamily: 3, gray) │
│ │
│ ┌─ Service A (strokeWidth: 1, service color) ────────┐ │
│ │ Service A Label (fontSize: 14) │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─ Service B ─────────────────────────────────────────┐ │
│ │ Service B Label │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─ Small A ──────┐ ┌─ Small B ──────┐ │
│ │ (fontSize: 13) │ │ (fontSize: 13) │ │
│ └────────────────┘ └────────────────┘ │
└──────────────────────────────────────────────────────────┘
Arrows always have a separate text element as their label:
// Arrow element
{ "id": "arrow-a-to-b", "type": "arrow", ... },
// Label element (positioned above the arrow midpoint)
{ "id": "arrow-a-to-b-label", "type": "text", "fontSize": 11, "fontFamily": 3, ... }- Active server: solid border,
strokeColor: "#2f9e44",backgroundColor: "#ebfbee" - Standby server: dashed border (
strokeStyle: "dashed"), same colors but dashed - Bisync arrow between them:
strokeColor: "#e03131", bi-directional arrowheads
Use a diamond shape with amber coloring:
{
"type": "diamond",
"strokeColor": "#f08c00",
"backgroundColor": "#fff3bf",
"strokeWidth": 2
}The recommended tool for converting .excalidraw files to SVG/PNG is excalirender via Docker:
# Single file to SVG
docker run --rm --platform linux/amd64 \
-v "$(pwd)/diagrams:/data" -w /data \
jonarc06/excalirender:latest \
diagram.excalidraw --format svg -o diagram.svg
# Single file to PNG
docker run --rm --platform linux/amd64 \
-v "$(pwd)/diagrams:/data" -w /data \
jonarc06/excalirender:latest \
diagram.excalidraw --format png -o diagram.png
# With scale factor (for higher resolution PNG)
docker run --rm --platform linux/amd64 \
-v "$(pwd)/diagrams:/data" -w /data \
jonarc06/excalirender:latest \
diagram.excalidraw --format png --scale 2 -o diagram@2x.png
# Recursive (all .excalidraw files in directory)
docker run --rm --platform linux/amd64 \
-v "$(pwd)/diagrams:/data" -w /data \
jonarc06/excalirender:latest \
--recursive --format svgNotes:
--platform linux/amd64is required on ARM hosts (macOS Apple Silicon, AWS Graviton). The image is x86_64 only and runs via QEMU emulation (~1 second per diagram).- Output SVGs include embedded fonts (Helvetica, Cascadia) so they render correctly without network access.
- The Docker image is
jonarc06/excalirender:latest(~50MB).
Standard Makefile target for batch conversion:
EXCALIRENDER_IMAGE := jonarc06/excalirender:latest
DIAGRAMS_SRC := docs/assets/diagrams
DIAGRAMS_OUT := docs/assets/images
diagrams: ## Convert Excalidraw diagrams to SVG
@echo "Converting Excalidraw diagrams to SVG..."
@for f in $(DIAGRAMS_SRC)/*.excalidraw; do \
name=$$(basename "$$f" .excalidraw); \
echo " $$name.excalidraw -> $$name.svg"; \
docker run --rm --platform linux/amd64 \
-v "$$(pwd)/$(DIAGRAMS_SRC):/data" -w /data \
$(EXCALIRENDER_IMAGE) \
"$$name.excalidraw" --format svg -o "/data/$$name.svg"; \
mv "$(DIAGRAMS_SRC)/$$name.svg" "$(DIAGRAMS_OUT)/$$name.svg"; \
done
@echo "Diagrams converted"| Tool | Install | Pros | Cons |
|---|---|---|---|
| excalirender (Docker) | docker pull jonarc06/excalirender |
Best rendering quality, embedded fonts, SVG+PNG+PDF, no native deps | x86_64 only (needs QEMU on ARM), requires Docker |
excalidraw-to-svg (npm) |
npm install excalidraw-to-svg |
Pure Node.js, no native deps | Requires canvas npm package for some elements; broken with recent Node versions |
excalidraw_export (npm) |
npm install -g excalidraw_export |
Has --embed-fonts option |
Requires native canvas (Cairo/Pango); build issues on ARM |
<figure markdown="span">
<img src="../../assets/images/diagram-name.svg" alt="Description" style="width: 100%; max-width: 1200px;">
<figcaption>Caption text (revision YYYY-MM-DD)</figcaption>
</figure>Before committing a new .excalidraw file, verify:
- All elements have
roughness: 0 - Text uses
fontFamily: 2(Helvetica) orfontFamily: 3(Cascadia for code) - Font sizes follow the hierarchy (18 → 16 → 14 → 13 → 12 → 11)
- Colors follow the semantic palette (not arbitrary colors)
- Group containers use
strokeWidth: 2, inner elements usestrokeWidth: 1 - All arrows have proper
startBinding/endBindingwith matchingboundElementson targets - Arrow labels are positioned above/beside the arrow with matching color
- Element IDs are descriptive kebab-case
- Each
seedandversionNonceis unique -
textandoriginalTextfields match on all text elements - Standby/inactive elements use
strokeStyle: "dashed"and/or reducedopacity: 60 - SVG has been regenerated via
make diagramsordocker run excalirender