name: excalidraw description: Generate Excalidraw diagrams from descriptions. Creates .excalidraw JSON files that can be opened in https://aka.ms/excalidraw or embedded in documentation. Use when creating architecture diagrams, flowcharts, data flow visualizations, or illustrations for proposals and design documents.
Excalidraw JSON Format
When generating Excalidraw .excalidraw files programmatically, follow these requirements.
File Structure
{
"type": "excalidraw",
"version": 2,
"source": "claude",
"elements": [...],
"appState": {"viewBackgroundColor": "#ffffff"}
}
Critical: Text Elements Require Explicit Dimensions
Text elements MUST have width and height properties, otherwise they render as 0-size invisible boxes.
{
"type": "text",
"x": 50,
"y": 20,
"width": 600,
"height": 35,
"text": "Your text here",
"fontSize": 28,
"fontFamily": 1,
"strokeColor": "#000000",
"id": "unique_id",
"textAlign": "left",
"verticalAlign": "top"
}
Dimension Guidelines
- Estimate
widthbased on text length and fontSize (~10-12px per character at fontSize 14) - Estimate
heightbased on fontSize and line count: usefontSize * 2.5 * lineCountto avoid truncation- The Virgil hand-drawn font (fontFamily: 1) needs extra vertical space
- This is much more generous than typical font metrics but necessary for proper display
- Example: 4 lines at fontSize 12 → height of
12 * 2.5 * 4 = 120 - Single line title at fontSize 28 → height of
28 * 2.5 = 70
- For multi-line text, use
\nin the text string - When in doubt, round height UP significantly - slightly too large is far better than truncated
Common Element Types
Rectangle (Leaf Element with Fill)
{
"type": "rectangle",
"x": 30,
"y": 70,
"width": 200,
"height": 100,
"strokeColor": "#1864ab",
"backgroundColor": "#a5d8ff",
"fillStyle": "hachure",
"strokeWidth": 1,
"roundness": { "type": 3 },
"id": "unique_id"
}
Rectangle (Container - Transparent)
{
"type": "rectangle",
"x": 20,
"y": 50,
"width": 300,
"height": 400,
"strokeColor": "#1864ab",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"roundness": { "type": 3 },
"id": "container_id"
}
Ellipse
{
"type": "ellipse",
"x": 100,
"y": 100,
"width": 150,
"height": 80,
"strokeColor": "#2f9e44",
"backgroundColor": "#b2f2bb",
"fillStyle": "hachure",
"strokeWidth": 2,
"id": "unique_id"
}
Arrow
{
"type": "arrow",
"x": 200,
"y": 150,
"width": 100,
"height": 50,
"strokeColor": "#2f9e44",
"strokeWidth": 2,
"points": [
[0, 0],
[100, 50]
],
"id": "unique_id"
}
Arrow Bindings
To anchor arrows to shapes:
{
"startBinding": { "elementId": "source-id", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "target-id", "focus": 0, "gap": 1 }
}
Styling: Color-Coded Sections with Transparent Containers
Use color to differentiate logical sections, but keep it tasteful:
Critical Rule: Container Boxes Must Be Transparent
Any box that contains other elements (wrapper/container boxes) MUST have "backgroundColor": "transparent".
Only leaf elements (boxes that don't contain other boxes) should have fills. This prevents visual clutter and ensures inner elements remain visible.
// GOOD: Container box is transparent
{
"type": "rectangle",
"id": "container",
"strokeColor": "#1864ab",
"backgroundColor": "transparent",
"strokeWidth": 2
}
// GOOD: Inner leaf box has fill
{
"type": "rectangle",
"id": "inner_item",
"strokeColor": "#1864ab",
"backgroundColor": "#a5d8ff",
"fillStyle": "hachure"
}
Recommended Color Palette (Open Color)
Use matching border and fill colors per section:
| Section Type | Border | Fill (light) | Use Case |
|---|---|---|---|
| Blue | #1864ab |
#a5d8ff |
Data sources, inputs |
| Orange | #e67700 |
#fff3bf |
Processing, signals |
| Purple | #862e9c |
#f3d9fa |
Graphs, relationships |
| Green | #2f9e44 |
#b2f2bb |
Core logic, ML/AI |
| Teal | #0c8599 |
#99e9f2 |
Outputs, consumers |
| Gray | #495057 |
#dee2e6 |
Neutral, annotations |
FluentUI-Aligned Colors (for Microsoft Projects)
For diagrams in Microsoft/FluentUI contexts, use these colors for design consistency:
| Purpose | Stroke | Background | Notes |
|---|---|---|---|
| Primary/Brand | #0078D4 |
#CFE4FA |
Microsoft Blue |
| Success | #107C10 |
#DFF6DD |
Green for positive flows |
| Error/Danger | #D13438 |
#FDE7E9 |
Red for errors |
| Warning | #F7630C |
#FFF4CE |
Orange for caution |
| Accent | #5C2D91 |
#E8DAEF |
Purple for emphasis |
| Neutral | #1e1e1e |
#F3F2F1 |
Default |
Text Color: Always Black
All text elements MUST use "strokeColor": "#000000" (black). Colored text on hachure-filled backgrounds is hard to read. Use box border/fill colors to convey grouping, not text color.
What to Avoid
- Colored text on filled boxes (use black text instead)
- Filling container/wrapper boxes (makes inner elements hard to see)
- Mismatched border and fill color families
- Too many different colors in one diagram (stick to 3-5 section colors max)
Font Families
1= Virgil (hand-drawn style, default)2= Helvetica3= Cascadia (monospace)
Bound Text (Container Labels)
To make text scale with its container, bind text to a shape:
- Add
boundElementsarray to the container:
{
"type": "rectangle",
"id": "my_box",
"boundElements": [{"type": "text", "id": "my_box_text"}],
...
}
- Add
containerIdto the text element:
{
"type": "text",
"id": "my_box_text",
"containerId": "my_box",
"textAlign": "center",
"verticalAlign": "middle",
...
}
Bound Text Notes
- Text alignment typically uses
"center"and"middle"for bound text - The container will auto-resize based on text content when edited
- Works with rectangles, ellipses, diamonds, and other shapes
Best Practices
- Use unique
idvalues for all elements - Position elements on a grid (multiples of 10 or 20)
- Keep consistent spacing between related elements
- Use color coding to group related concepts (matching border + fill per section)
- Container boxes must be transparent - only fill leaf elements
- All text must be black (
#000000) - use box colors for grouping, not text colors - Add key insight boxes at the top of diagrams with distinctive styling
- Include token counts or metrics on arrows to show data flow
- Prefer bound text for labels inside shapes - this ensures text scales with containers
- For standalone text (not in shapes), use very generous
heightvalues:fontSize * 2.5 * lineCount
Loading and Exporting via Browser
Use the Microsoft internal Excalidraw instance (https://aka.ms/excalidraw). Do NOT use excalidraw.com — it violates compliance rules.
Important: CSP restrictions
The MS-internal Excalidraw instance (aka.ms/excalidraw, hosted on jolly-ground-0a6b3831e.4.azurestaticapps.net) has strict Content Security Policy headers that block fetch() to 127.0.0.1 and localhost. Do NOT use a local HTTP server as a data bridge — it will fail with CSP violations.
Loading a diagram into Excalidraw
Use Playwright's native fileChooser event combined with a showOpenFilePicker override. This bypasses CSP entirely because no network requests are made.
Why the override is needed
- Excalidraw uses the File System Access API (
showOpenFilePicker/showSaveFilePicker), not<input type="file">— sobrowser_file_uploaddoes not work directly. - DragEvent drop is rejected by Excalidraw with "Couldn't load invalid file" — do not attempt drag-and-drop.
- The override converts the File System Access API call into a hidden
<input type="file">that Playwright'sfileChooserevent can intercept.
CRITICAL: The MS Excalidraw instance does NOT show a "Load from file" confirmation dialog. Clicking the Open button directly invokes showOpenFilePicker. Therefore the override MUST be set up BEFORE the Open button is clicked, and everything must happen in a single browser_run_code call.
Step-by-step flow
Navigate to
https://aka.ms/excalidrawand wait for it to load (SSO signs in automatically):browser_navigate to https://aka.ms/excalidraw Wait ~5 seconds for SSO + canvas to loadIf SSO doesn't auto-complete (e.g. "Pick an account" screen appears), click the user's account to finish sign-in, then wait for the canvas to load.
Load the file in a single atomic
browser_run_codecall — overrideshowOpenFilePicker, then click menu → Open, and catch the file chooser:async (page) => { // Step 1: Override showOpenFilePicker BEFORE any menu clicks await page.evaluate(() => { window.showOpenFilePicker = () => { return new Promise((resolve) => { const input = document.createElement("input"); input.type = "file"; input.accept = ".excalidraw,.json"; input.style.display = "none"; input.addEventListener("change", () => { const file = input.files[0]; resolve([{ getFile: async () => file }]); input.remove(); }); document.body.appendChild(input); input.click(); }); }; }); // Step 2: Click the hamburger menu await page.locator('[data-testid="main-menu-trigger"]').click(); await page.waitForTimeout(800); // Step 3: Click Open and catch the file chooser simultaneously const [fileChooser] = await Promise.all([ page.waitForEvent("filechooser", { timeout: 10000 }), page.locator('[data-testid="load-button"]').click(), ]); // Step 4: Set the file await fileChooser.setFiles("/absolute/path/to/diagram.excalidraw"); await page.waitForTimeout(3000); return "Loaded!"; };Scroll to content and frame the diagram — the diagram loads off-screen. Use
browser_snapshotto check for a "Scroll back to content" button and click it. Then zoom out using the zoom-out button (button[aria-label="Zoom out"]) repeatedly until the full diagram fits in the viewport. Do NOT useCtrl+Shift+1— this shortcut does not work on the MS Excalidraw instance. There is also no publicwindow.excalidrawAPIavailable for programmatic zoom.
Exporting to PNG/SVG
Quick export (recommended): Screenshot
After loading the diagram, click "Scroll back to content" to center it, then use browser_take_screenshot to capture a PNG. This includes UI chrome but is fast and reliable:
browser_take_screenshot with filename "diagram.png"
Full export (no UI chrome)
Override showSaveFilePicker to capture the exported binary, then use Playwright's run_code to write it to disk:
async (page) => {
// Set up a promise to capture the exported data
await page.evaluate(() => {
window._exportedData = null;
window._exportReady = new Promise((resolve) => {
window._exportResolve = resolve;
});
window.showSaveFilePicker = async (opts) => ({
createWritable: async () => {
const chunks = [];
return {
write: async (data) => {
const buf = data instanceof Blob ? new Uint8Array(await data.arrayBuffer()) : data;
chunks.push(buf);
},
close: async () => {
const total = chunks.reduce((s, c) => s + c.length, 0);
const merged = new Uint8Array(total);
let off = 0;
for (const c of chunks) {
merged.set(c, off);
off += c.length;
}
// Store as base64 for retrieval
let binary = "";
for (let i = 0; i < merged.length; i++) binary += String.fromCharCode(merged[i]);
window._exportedData = btoa(binary);
window._exportResolve();
},
};
},
});
});
// Click: menu → "Export image..." → "Export to PNG"
await page.locator('[data-testid="main-menu-trigger"]').click();
await page.locator('[data-testid="image-export-button"]').click();
await page.locator('[aria-label="Export to PNG"]').click();
// Wait for export and retrieve the base64 data
await page.evaluate(() => window._exportReady);
const b64 = await page.evaluate(() => window._exportedData);
// Write to disk (b64 is returned to the caller for saving via bash)
return b64;
};
Then save the base64 data to a file using Node (cross-platform — works on macOS, Linux, and Windows):
node -e "require('fs').writeFileSync('diagram.png', Buffer.from(process.argv[1], 'base64'))" "<base64_data>"
Note: If the base64 string is very large, the browser_run_code return value may be truncated. In that case, retrieve the data in chunks to avoid size limits:
// After export is ready, retrieve in chunks:
const size = await page.evaluate(() => window._exportedData.length);
const CHUNK = 50_000;
let out = "";
for (let start = 0; start < size; start += CHUNK) {
out += await page.evaluate(([s, c]) => window._exportedData.slice(s, s + c), [start, CHUNK]);
}
// Then save: node -e "require('fs').writeFileSync('diagram.png', Buffer.from(process.argv[1], 'base64'))" "$out"
If chunked retrieval still fails, browser_take_screenshot can be used as a last resort (note: this includes UI chrome).
Preserving Source Files
Always save the .excalidraw file alongside any converted PNG/SVG for future editing.