Claude
Skills
Sign in
Back

Excalidraw Generation

Included with Lifetime
$97 forever

This skill should be used when the user asks to "create excalidraw diagram", "generate excalidraw", "hand-drawn diagram", "sketch diagram", "whiteboard style diagram", or when informal, spatial, annotated diagrams would best convey conceptual relationships. Expert in both presentation design AND artistic Excalidraw JSON creation.

Design

What this skill does


# Excalidraw Generation Expert

**Core Philosophy**: Semantic redesign, not mechanical conversion. Think like both a presentation designer (clarity, accessibility, simplicity) and an artist (creative visual expression, spatial design, aesthetic beauty).

## CRITICAL - Rendering Rule

**ALWAYS use render-excalidraw.sh for SVG conversion - NO EXCEPTIONS**

After creating Excalidraw JSON:
1. Save JSON to `diagrams/<slug>.excalidraw`
2. **MUST** render using: `${CLAUDE_PLUGIN_ROOT}/scripts/render-excalidraw.sh`
3. NEVER attempt manual SVG conversion
4. NEVER embed JSON in markdown - only reference the rendered SVG

The script handles all rendering automatically with excalidraw-brute-export-cli.

## When to Use This Skill

**Auto-trigger when:**
- User explicitly requests: "create excalidraw diagram", "hand-drawn diagram", "sketch", "whiteboard"
- Slide content suggests conceptual/spatial relationships
- Architecture with nested components
- Brainstorming/ideation context
- Informal, approachable style needed
- Annotations and callouts would add value

**Diagram type suitability:**
- ✅✅✅ BEST: Conceptual relationships, architecture diagrams, mind maps, timelines, comparisons
- ✅✅ EXCELLENT: Flowcharts (with annotations), spatial layouts, nested structures
- ⚠️ OKAY: Sequence diagrams (prefer Mermaid instead)
- ❌ NOT RECOMMENDED: Formal UML (use PlantUML), state machines (use Mermaid)

## Evidence-Based Design Constraints (HARD LIMITS)

These constraints are NON-NEGOTIABLE. Enforce strictly:

- **Cognitive load**: Maximum 9 elements (7±2 rule from cognitive psychology)
- **Accessibility**:
  - Colorblind-safe palette ONLY: Blue #3b82f6 + Orange #f97316
  - Minimum 4.5:1 contrast ratio for all text (WCAG AA)
  - Never rely on color alone to convey information
- **Minimal text**: Under 50 words total per diagram
- **One idea per diagram**: If concept is complex, split into multiple diagrams
- **Hand-drawn aesthetic**: Roughness 1 for informal feel

## Core Capabilities

### 1. Semantic Concept Extraction

**Process** (ALWAYS follow this order):

1. **Analyze user's description or slide content**
2. **Extract core concepts** (entities, relationships, flows)
3. **Identify semantic type**:
   - **Containment**: X contains Y → Use nested boxes/frames
   - **Flow**: A→B→C → Use arrows with spatial progression
   - **Comparison**: X vs Y → Use side-by-side separation
   - **Hierarchy**: Parent-child → Use vertical/spatial positioning
   - **Grouping**: Related items → Use frames or color-coded regions
   - **Annotation**: Context/explanation → Use callouts and bound text

4. **Design layout** (choose from layout algorithms below)
5. **Generate JSON** (use element factories below)

**Example**:
```
Input: "Kubernetes device plugin architecture"

Semantic analysis:
- Type: Architecture + Flow
- Key concepts: Control Plane (container), Worker Node (container),
                GPU (component), Device Plugin (component), Kubelet (component)
- Relationships: Discovery flow, Registration flow, Capacity updates
- Spatial meaning: Control Plane ABOVE Worker Node (hierarchy)

Design choice: Vertical layout with 2 frames, 5 shapes, 3 arrows, 3 annotations
Cognitive load: 2 frames + 5 shapes = 7 units ✓
```

### 2. Element Factories

These functions generate valid Excalidraw JSON elements. Use them to build diagrams.

#### ID Generation

```javascript
function generateId() {
  // Excalidraw uses random alphanumeric IDs (12+ chars)
  return Math.random().toString(36).substring(2, 15) +
         Math.random().toString(36).substring(2, 15);
}
```

#### Rectangle Factory

```javascript
function createRectangle(x, y, width, height, text = null, options = {}) {
  const id = generateId();

  const element = {
    type: "rectangle",
    version: 1,
    versionNonce: Math.floor(Math.random() * 1000000),
    isDeleted: false,
    id: id,
    fillStyle: options.fillStyle || "hachure",
    strokeWidth: options.strokeWidth || 2,
    strokeStyle: "solid",
    roughness: options.roughness !== undefined ? options.roughness : 1,
    opacity: 100,
    angle: options.angle || 0,
    x: x,
    y: y,
    strokeColor: options.strokeColor || THEME_COLORS.primary,
    backgroundColor: options.backgroundColor || "transparent",
    width: width,
    height: height,
    seed: Math.floor(Math.random() * 1000000),
    groupIds: options.groupIds || [],
    frameId: options.frameId || null,
    roundness: { type: 3 },
    boundElements: [],
    updated: Date.now(),
    link: null,
    locked: false
  };

  // If text provided, create bound text element
  if (text) {
    const textElement = createBoundText(text, id, x, y, width, height);
    element.boundElements.push({ type: "text", id: textElement.id });
    return [element, textElement];  // Return array
  }

  return element;  // Return single element
}
```

#### Text Factory (Standalone and Bound)

```javascript
function createText(text, x, y, options = {}) {
  return {
    type: "text",
    version: 1,
    versionNonce: Math.floor(Math.random() * 1000000),
    isDeleted: false,
    id: options.id || generateId(),
    fillStyle: "hachure",
    strokeWidth: 1,
    strokeStyle: "solid",
    roughness: 0,  // Text is always smooth
    opacity: 100,
    angle: 0,
    x: x,
    y: y,
    strokeColor: options.strokeColor || THEME_COLORS.text,
    backgroundColor: "transparent",
    width: options.width || 200,
    height: options.height || 25,
    seed: Math.floor(Math.random() * 1000000),
    groupIds: options.groupIds || [],
    frameId: options.frameId || null,
    roundness: null,
    boundElements: [],
    updated: Date.now(),
    link: null,
    locked: false,
    fontSize: options.fontSize || 20,
    fontFamily: 1,  // 1 = Excalifont/Virgil (hand-drawn), 2 = Helvetica, 3 = Cascadia
    text: text,
    textAlign: options.textAlign || "center",
    verticalAlign: options.verticalAlign || "middle",
    containerId: options.containerId || null,
    originalText: text,
    lineHeight: 1.25,
    baseline: 18
  };
}

// Font family mapping for reference:
// fontFamily: 1 → Excalifont/Virgil (hand-drawn, default for Excalidraw aesthetic)
// fontFamily: 2 → Helvetica (clean, modern)
// fontFamily: 3 → Cascadia (monospace, code)
// When rendering to SVG: font-family: 'Excalifont', 'Virgil', cursive, sans-serif


function createBoundText(text, containerId, containerX, containerY, containerWidth, containerHeight) {
  // Calculate centered position inside container
  const textWidth = Math.min(containerWidth - 20, 200);
  const textHeight = 25;
  const textX = containerX + (containerWidth - textWidth) / 2;
  const textY = containerY + (containerHeight - textHeight) / 2;

  return createText(text, textX, textY, {
    width: textWidth,
    height: textHeight,
    containerId: containerId,
    textAlign: "center",
    verticalAlign: "middle"
  });
}
```

#### Arrow Factory

```javascript
function createArrow(startX, startY, endX, endY, options = {}) {
  const points = [
    [0, 0],  // Start point (relative to x, y)
    [endX - startX, endY - startY]  // End point (relative)
  ];

  return {
    type: "arrow",
    version: 1,
    versionNonce: Math.floor(Math.random() * 1000000),
    isDeleted: false,
    id: generateId(),
    fillStyle: "hachure",
    strokeWidth: options.strokeWidth || 2,
    strokeStyle: "solid",
    roughness: options.roughness !== undefined ? options.roughness : 1,
    opacity: 100,
    angle: 0,
    x: startX,
    y: startY,
    strokeColor: options.strokeColor || THEME_COLORS.neutral,
    backgroundColor: "transparent",
    width: Math.abs(endX - startX),
    height: Math.abs(endY - startY),
    seed: Math.floor(Math.random() * 1000000),
    groupIds: options.groupIds || [],
    frameId: options.frameId || null,
    roundness: { type: 2 },
    boundElements: [],
    updated: Date.now(),
    link: null,
    locked: false,
    startBinding: options.startBinding || null,
    endBinding: options.endBinding || null,
    lastCommittedPoint: null,

Related in Design