Skip to content

Instantly share code, notes, and snippets.

@needcaffeine
Created February 12, 2026 16:29
Show Gist options
  • Select an option

  • Save needcaffeine/8126271cd9be75056130f39b89c7240e to your computer and use it in GitHub Desktop.

Select an option

Save needcaffeine/8126271cd9be75056130f39b89c7240e to your computer and use it in GitHub Desktop.
description allowed-tools
Create and edit Excalidraw diagrams — generate, modify, and clean .excalidraw files
Read
Write
Edit
Bash
Glob

Excalidraw Diagram Skill

You can create and edit .excalidraw files directly as JSON. No CLI needed.

File Structure

{
  "type": "excalidraw",
  "version": 2,
  "source": "https://excalidraw.com",
  "elements": [ ... ],
  "appState": {
    "gridSize": 20,
    "gridStep": 5,
    "gridModeEnabled": false,
    "viewBackgroundColor": "#ffffff"
  },
  "files": {}
}

Element Defaults

Every element needs these base properties. Use these defaults unless overridden:

{
  "version": 1,
  "versionNonce": <random 9-digit integer>,
  "isDeleted": false,
  "fillStyle": "solid",
  "strokeWidth": 2,
  "strokeStyle": "solid",
  "roughness": 2,
  "opacity": 100,
  "angle": 0,
  "strokeColor": "#1e1e1e",
  "backgroundColor": "transparent",
  "seed": <random 9-digit integer>,
  "groupIds": [],
  "frameId": null,
  "roundness": null,
  "boundElements": [],
  "updated": <Date.now() timestamp>,
  "link": null,
  "locked": false
}

Element IDs

Generate short descriptive IDs for new elements (e.g., "push_box", "arrow_push_to_merge"). For bound text labels, append _label (e.g., "push_box_label").

Index (Element Ordering)

Elements use fractional indexing strings for z-order: "a0", "a1", ... "a9", "aA", ... "aZ", "aa", "ab", etc. When adding elements to an existing file, continue from the highest existing index.

Shapes

Rectangle, Ellipse, Diamond

{
  "type": "rectangle",
  "id": "my_box",
  "x": 100, "y": 200,
  "width": 200, "height": 80,
  "roundness": { "type": 3 },
  "backgroundColor": "#a5d8ff",
  "fillStyle": "solid",
  "boundElements": [
    { "id": "my_box_label", "type": "text" }
  ]
}
  • type: "rectangle", "ellipse", or "diamond"
  • roundness: { "type": 3 } = rounded corners (use for rectangles by default)
  • roundness: { "type": 2 } = smooth curves (use for ellipses/diamonds)
  • roundness: null = sharp corners

Diamond (decision/gate nodes)

{
  "type": "diamond",
  "id": "gate",
  "x": 600, "y": 150,
  "width": 130, "height": 90,
  "roundness": { "type": 2 },
  "backgroundColor": "#fff3bf",
  "fillStyle": "solid",
  "boundElements": [
    { "id": "gate_label", "type": "text" }
  ]
}

Diamonds render as rotated squares. The width/height define the bounding box — the visible diamond touches the midpoints of each side. Size diamonds ~1.5x larger than you'd expect since the usable interior is smaller.

Fill styles

  • "solid" — flat color fill (default, clean look)
  • "hachure" — diagonal line fill (classic excalidraw hand-drawn feel)
  • "cross-hatch" — cross-hatched lines (good for "proposed" or "in progress" elements)

Roughness levels

  • 0 = Architect — clean, precise lines
  • 1 = Artist — slight wobble
  • 2 = Cartoonist — full hand-drawn feel (default)

Common colors

Color Hex Use for
Blue #a5d8ff Standard steps
Purple #d0bfff Tooling / automation
Green #b2f2bb Success / prod deploy
Light green #c3fae8 Positive state
Orange #ffd8a8 Manual / warning
Yellow #ffd43b Highlighted / attention
Light yellow #fff3bf Decision gates
Red bg #ffc9c9 Problem / danger
Red stroke #ef4444 Problem border/text
Gray text #757575 Annotations

Text Elements

Standalone text (labels, annotations)

{
  "type": "text",
  "id": "my_label",
  "x": 100, "y": 200,
  "width": 0, "height": 0,
  "text": "Some label",
  "fontSize": 16,
  "fontFamily": 5,
  "textAlign": "left",
  "verticalAlign": "top",
  "containerId": null,
  "originalText": "Some label",
  "autoResize": true,
  "lineHeight": 1.25
}

Note: width: 0, height: 0 is fine — excalidraw auto-calculates on first render.

Bound text (label inside a shape)

{
  "type": "text",
  "id": "my_box_label",
  "x": 110, "y": 210,
  "width": 180, "height": 60,
  "text": "Box Label",
  "fontSize": 16,
  "fontFamily": 5,
  "textAlign": "center",
  "verticalAlign": "middle",
  "containerId": "my_box",
  "originalText": "Box Label",
  "autoResize": true,
  "lineHeight": 1.25
}

Critical: The parent shape's boundElements must include { "id": "my_box_label", "type": "text" }.

Font families

  • 5 = Excalidraw default (hand-drawn, current — use this)
  • 1 = Virgil (hand-drawn, legacy)
  • 2 = Helvetica (clean sans-serif)
  • 3 = Cascadia (monospace — good for code/technical labels)

Font size guidelines

  • Title: 28
  • Normal label: 16-18
  • Annotation/note: 14

Lines

Plain lines without arrowheads — useful for separators, connectors, and underlines.

{
  "type": "line",
  "id": "separator",
  "x": 100, "y": 300,
  "width": 400, "height": 0,
  "points": [[0, 0], [400, 0]],
  "startArrowhead": null,
  "endArrowhead": null
}
  • Same point system as arrows: x, y is origin, points are relative
  • Can have multiple points for polylines
  • Supports strokeStyle: "solid", "dashed", "dotted"
  • Add "roundness": { "type": 2 } for smooth curves through points

Arrows

{
  "type": "arrow",
  "id": "arrow_a_to_b",
  "x": 300, "y": 240,
  "width": 50, "height": 0,
  "points": [[0,0], [50, 0]],
  "startBinding": {
    "elementId": "box_a",
    "mode": "orbit",
    "fixedPoint": [1, 0.5]
  },
  "endBinding": {
    "elementId": "box_b",
    "mode": "orbit",
    "fixedPoint": [0, 0.5]
  },
  "startArrowhead": null,
  "endArrowhead": "arrow",
  "elbowed": false
}

Arrow position and points

  • x, y is the starting position of the arrow
  • points are relative to x, y: first point is always [0, 0]
  • width = max x extent, height = max y extent of the points

Arrow bindings

  • fixedPoint: [x, y] where x,y are 0-1 relative to the target element
    • [0, 0.5] = left center
    • [1, 0.5] = right center
    • [0.5, 0] = top center
    • [0.5, 1] = bottom center
  • mode: "orbit" (default) or "inside"
  • Set to null for unbound arrows

Critical: The target shape's boundElements must include { "id": "arrow_a_to_b", "type": "arrow" }.

Arrow styles

  • endArrowhead: "arrow" (default), "triangle", "bar", "dot", null
  • startArrowhead: same options (for bidirectional arrows)
  • strokeStyle: "solid", "dashed", "dotted"

Curved arrows (multi-point)

Use 3+ points with roundness for smooth curves:

{
  "type": "arrow",
  "id": "curved_arrow",
  "x": 300, "y": 240,
  "width": 150, "height": 100,
  "points": [[0, 0], [75, 50], [150, 100]],
  "roundness": { "type": 2 },
  "startBinding": null,
  "endBinding": null,
  "startArrowhead": null,
  "endArrowhead": "arrow",
  "elbowed": false
}

The middle point(s) act as control points — the arrow curves smoothly through them.

Elbowed (right-angle) arrows

{
  "elbowed": true,
  "fixedSegments": [
    { "index": 2, "start": [0, 35], "end": [80, 35] }
  ]
}

Groups

Group elements so they're logically associated. All grouped elements share the same group ID in their groupIds array.

// Element A
{ "id": "box_a", "groupIds": ["group_pipeline"], ... }
// Element B
{ "id": "box_b", "groupIds": ["group_pipeline"], ... }
// Arrow between them
{ "id": "arrow_ab", "groupIds": ["group_pipeline"], ... }
  • Generate group IDs as descriptive strings (e.g., "group_pipeline", "group_deploy")
  • An element can belong to multiple groups (nested grouping)
  • Groups don't have their own element entry — they exist only via groupIds references
  • In excalidraw UI, grouped elements select together on click

Frames

Named containers that visually group a section of a diagram. Frames render as a labeled rectangle with a title.

{
  "type": "frame",
  "id": "frame_initiative1",
  "x": 50, "y": 50,
  "width": 600, "height": 400,
  "name": "Initiative 1: Feature Branches",
  "roundness": null,
  "boundElements": [],
  "version": 1, "versionNonce": 123456789,
  "isDeleted": false, "fillStyle": "solid",
  "strokeWidth": 2, "strokeStyle": "solid",
  "roughness": 0, "opacity": 100, "angle": 0,
  "strokeColor": "#bbb", "backgroundColor": "transparent",
  "seed": 987654321, "groupIds": [], "frameId": null,
  "updated": 1770865000000, "link": null, "locked": false,
  "index": "a0"
}
  • Child elements point to the frame via "frameId": "frame_initiative1"
  • Frame name renders as a label above the frame
  • Use roughness: 0 for frames (clean lines look better)
  • Size the frame to contain all child elements with ~20px padding

Links

Make elements clickable by setting the link property:

{
  "id": "my_box",
  "link": "https://example.com/docs",
  ...
}
  • Works on any element type
  • In excalidraw UI, linked elements show a link icon on hover
  • Useful for linking diagram nodes to docs, Jira tickets, etc.

Opacity

Control transparency with opacity (0-100):

  • 100 = fully opaque (default)
  • 50 = semi-transparent (good for background/context elements)
  • 25 = ghost/watermark effect

Useful for "before/after" diagrams — show the old flow at low opacity with the new flow at full opacity on top.

Element Locking

Prevent accidental edits on finalized elements:

{ "id": "my_box", "locked": true, ... }

Operations

Creating a new file

  1. Build the elements array with all shapes, text labels, and arrows
  2. Ensure all bindings are bidirectional (shape references arrow AND arrow references shape)
  3. Assign sequential index values starting from "a0"
  4. Wrap in the file structure and write with the Write tool

Adding elements to an existing file

  1. Read the existing file
  2. Find the highest index value: jq '[.elements[] | .index] | sort | last'
  3. Create new elements with indices after the highest
  4. Add new elements to the elements array
  5. Update any existing elements' boundElements if new arrows connect to them
  6. Write back with jq via Bash for surgical edits:
    jq '<mutations>' file.excalidraw > _tmp.excalidraw && mv _tmp.excalidraw file.excalidraw

Editing elements

Use the Edit tool to find and replace specific JSON properties in the file.

Deleting elements

Set "isDeleted": true on the element. Don't remove it from the array — excalidraw uses tombstones.

Cleaning up

Find and mark as deleted:

  • Elements with null x/y coordinates
  • Text elements with containerId pointing to a deleted element
  • Arrows with bindings pointing to deleted elements
  • Use: jq '[.elements[] | select(.x == null) | select(.isDeleted == false or .isDeleted == null) | .id]'

Inspecting a file

List all active elements with positions:

jq '[.elements[] | select(.isDeleted == false or .isDeleted == null) | {id, type, x, y, width, height, text: (if .type == "text" then .text else null end)} | del(.[] | nulls)]' file.excalidraw

Layout Guidelines

  • Standard box size: 180-220w x 70-80h
  • Diamond size: 130-150w x 90-110h (larger than boxes due to rotated shape)
  • Arrow gap between boxes: 40-60px
  • Row spacing: 120-150px
  • Keep main flow horizontal, branches vertical
  • Total width ~1200px max for readability
  • Frame padding: ~20px around contained elements

Template Patterns

Flowchart row (evenly spaced boxes with arrows)

Box A (x=50)  --arrow(50px gap)-->  Box B (x=300)  --arrow-->  Box C (x=550)

Formula: next_x = prev_x + prev_width + arrow_gap(50)

Fork/join (one input, multiple outputs)

            --> Box B (y - 70)
Box A --+
            --> Box C (y + 70)

Use two arrows from Box A with different fixedPoints: [1, 0.3] and [1, 0.7]

Decision gate

Box A --> ◇ Decision --> Box B (yes)
              |
              v
          Box C (no)

Diamond with arrows from right (yes) and bottom (no). Add standalone text labels "yes"/"no" near each arrow.

Before/after overlay

Show old flow at opacity: 30 with new flow at opacity: 100 on top. Use strokeStyle: "dashed" on removed elements.

Checklist Before Writing

  1. Every shape with a label has a matching text element with containerId
  2. Every shape with a label has boundElements including the text element
  3. Every arrow binding target has boundElements including the arrow
  4. All originalText matches text
  5. All indices are sequential and unique
  6. No null coordinates on any active element
  7. Groups: all elements in a group share the same groupIds entry
  8. Frames: all child elements have frameId pointing to the frame

Instructions

When asked to create or edit an excalidraw diagram:

  1. If editing, READ the existing file first
  2. Plan the layout with coordinates before writing
  3. Build complete, valid JSON — don't skip properties
  4. Double-check all bindings are bidirectional
  5. Write the file directly using Write or Edit tools
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment