Skip to main content

Diagrams

The toolbar's insert-drawing button embeds a canvas in the document. The drawing persists in the markdown as a ```drawing fenced JSON block (format version 2); generators write a coordinate-free ```diagram skeleton instead. The full specification of both payloads ships in the package as DRAWING_FORMAT.md; this page covers the canvas and the essentials of each format.

The canvas

  • Shapes: rectangle, ellipse, diamond, note (sticky note), database (cylinder), cloud, queue (horizontal cylinder), actor (stick figure), arrow, line, text. The toolbar shows the common tools and a "more shapes" popover for the rest. Draw by picking a tool and dragging.
  • Cards: every box carries three text slots that move, resize and wrap with it: a bold label on top, content in the centre, a dim footer at the bottom (actor has only the name under the figure). Click a selected box (or double-click its top / middle / bottom strip, or press Enter) to edit a slot.
  • Bound connectors: an arrow drawn from one card to another attaches to both; moving a card moves its arrows, endpoints anchored to the outline. An endpoint either auto-aims at the other end or sticks to a fixed point on the box (a side, or a ratio inside it). Drag an endpoint off / onto a card to detach / re-attach.
  • Routing: straight (diagonal) by default; toggle elbow for an auto-routed right-angled path that leaves the box perpendicular to its attach side and avoids the other boxes, or drag the dashed + handles on a selected connector to add waypoints (they snap to neighbours' axes for clean 90° bends). One-way / two-way arrowhead toggle; midpoint labels.
  • Colour: one setting per shape (outline plus its matching pastel fill on boxes): plain (no outline), black, gray, red, green, blue, orange, purple. Text on dark or borderless cards picks a readable colour automatically.
  • Multi-select: shift-click, or drag a marquee on empty canvas, then move, delete or recolour the selection together. A contextual property row shows the options for whatever is selected.
  • Copy as Mermaid: a canvas button copies the drawing as a Mermaid flowchart (also available as drawingToMermaid(data)).
  • Canvas: resizable height (bottom handle), dot grid, white surface that inverts Excalidraw-style in dark mode. An optional canvasWidth scales the drawing to fit narrower layouts.
  • Width: buttons at the right end of the canvas toolbar switch between full, text and content (Layout). Stored as "width":"text" / "width":"content" in the payload, omitted when full.

In view mode the canvas renders read-only.

Loading editor

The ```diagram skeleton

Generators (LLMs, scripts) do not need coordinates. A ```diagram block holds a skeleton: boxes, connectors, colours. On import the editor lays it out along direction (right by default, or down), sizes boxes to their text, and stores the result as a normal ```drawing block. The expansion is one-way: once opened, the document holds the concrete drawing.

```diagram
{"boxes":[
{"id":"web","label":"CLIENT","text":"Web App","footer":"React","color":"blue"},
{"id":"api","label":"SERVICE","text":"API","footer":"Kotlin","color":"green"},
{"id":"db","type":"cylinder","text":"Postgres"}
],"connectors":[
{"from":"web","to":"api","text":"REST"},
{"from":"api","to":"db","routing":"elbow"}
]}
```

Shape of the skeleton:

FieldNotes
direction"right" (default) or "down": the auto-layout flow
canvasWidth, canvasHeight, widthOptional; copied to the expanded drawing
boxes[]Required. Each: id (unique), type (box type, default rect), label, text, footer, optional x / y (omitted: auto layout by connector rank), w / h (omitted: sized to the text), color (palette name) or explicit stroke / fill
connectors[]Each: from / to as a box id or {"id","side"} to pick the attach edge (top, right, bottom, left), type (arrow default, or line), text, bidirectional, routing (elbow or straight), color / stroke
texts[]Free annotations with explicit x, y, text, color

Auto layout ranks boxes along direction by the longest path from the sources (cycles are broken); ranks are 90px apart, boxes within a rank 40px apart, starting at (32, 32). A decision flowing down, with attach sides:

```diagram
{"direction":"down","boxes":[
{"id":"user","type":"actor","text":"User"},
{"id":"check","type":"diamond","text":"Valid?"},
{"id":"save","text":"Save","color":"green"},
{"id":"err","type":"note","text":"Show error","color":"red"}
],"connectors":[
{"from":"user","to":"check"},
{"from":{"id":"check","side":"bottom"},"to":"save","text":"yes","routing":"elbow"},
{"from":{"id":"check","side":"right"},"to":{"id":"err","side":"left"},"text":"no","routing":"elbow"}
]}
```
Loading editor

Programmatic access: parseDrawingSkeleton(json), expandSkeleton(skeleton)DrawingData, isDrawingSkeleton(value), DRAWING_SKELETON_JSON_SCHEMA, COLOR_PRESETS. Types DrawingSkeleton, SkeletonBox, SkeletonConnector, SkeletonEnd, SkeletonText, ColorName.

The ```drawing format

What the editor stores. One JSON object per fence:

```drawing
{"version":2,"canvasHeight":260,"shapes":[
{"id":"web","type":"rect","x":40,"y":70,"w":170,"h":100,"stroke":"#1971c2","fill":"#a5d8ff","strokeWidth":2,"label":"CLIENT","text":"Web App"},
{"id":"api","type":"rect","x":330,"y":70,"w":170,"h":100,"stroke":"#2f9e44","fill":"#b2f2bb","strokeWidth":2,"label":"SERVICE","text":"API"},
{"id":"e1","type":"arrow","x":216,"y":120,"w":108,"h":0,"stroke":"#1e1e1e","fill":"transparent","strokeWidth":2,"startBinding":{"id":"web"},"endBinding":{"id":"api"},"text":"REST"}
]}
```
Loading editor

Top level

FieldTypeNotes
version2Literal. Any other version (or none) renders as an empty canvas
canvasHeightnumberCanvas height in px (min 80; 300–400 is typical)
canvasWidthnumberOptional logical width (min 120); the drawing scales to fit when set, fluid when absent
width"full" | "text" | "content"Optional block width, default full
shapesShape[]Render order: later shapes draw on top

Origin is the canvas's top-left, units are CSS pixels. Without canvasWidth, keep shapes within roughly x ∈ [0, 700].

Shapes

All shapes share id, type, x, y, w, h, stroke, fill, strokeWidth (use 2). For boxes and text, x / y is the top-left and w / h the size; for connectors, x / y is the start and w / h the delta to the end.

TypeKindDefault sizeNotes
rectbox160 × 90The general-purpose card
ellipsebox160 × 90Inscribed in the bounding box
diamondbox170 × 100Decision; text area is the inner half
notebox160 × 110Sticky note, folded corner; default fill #ffec99
cylinderbox140 × 110Database
cloudbox180 × 110Outline is an ellipse for binding
queuebox180 × 80Horizontal cylinder
actorbox90 × 120Stick figure; text only, drawn under the figure
arrowconnectorHead at the end; bidirectional for both
lineconnectorNo arrowhead
textfree textFloating annotation; w / h advisory

Boxes carry the label / text / footer slots; text wraps to the box's text area and \n forces a line break. Connectors take text (midpoint label), bidirectional, startBinding / endBinding, routing: "elbow", elbow (0–1, middle-segment override for a simple Z) and waypoints ({x, y}[], which take precedence over routing).

Bindings

{ "id": "<box id>", "fixedPoint": [0.5, 1], "mode": "orbit" }

id is required (bindings to unknown or non-box ids are dropped). fixedPoint is a ratio of the box's bounding box ([0.5, 0] is the top edge midpoint); omit it to auto-aim from the box centre at the other end. mode is orbit (default: projected onto the outline with a 6px gap) or inside (pinned exactly). Bound endpoints are re-anchored by the editor, so a connector's own coordinates only need to be approximately right.

Palette

Any CSS colour works. The editor's own pairs, dark-mode safe (the canvas inverts colours in dark themes):

NameStrokeFill
plaintransparent#f1f3f5
black#1e1e1e#1e1e1e (text renders white)
gray#1e1e1etransparent
red#e03131#ffc9c9
green#2f9e44#b2f2bb
blue#1971c2#a5d8ff
orange#f08c00#ffec99
purple#7048e8#d0bfff

Malformed payloads never crash the editor: invalid shapes are dropped individually and anything that is not a version 2 payload renders as an empty canvas. parseDrawingData(json) is that lenient parser; serializeDrawingData(data) is its inverse; normalizeDrawingData re-anchors bindings; DRAWING_DATA_JSON_SCHEMA is the JSON Schema.

Mermaid export

drawingToMermaid(data, { direction?: 'LR' | 'TD' }) returns a flowchart: one node per box (shape-specific node syntax; text joins label, text and footer), one edge per connector bound at both ends (-->, <-->, ---; labels as |text|), a style line for every coloured box, and free text as %% note: comments. Unbound connectors are dropped. Without direction it picks LR when most edges run horizontally, else TD.

Ink style

drawingStyle="ink" renders every shape as a single pen stroke: the outline is displaced by a smooth, seeded wobble, the line width breathes along the path, and fills sit a couple of pixels off the outline like a print. Connectors taper toward their ends. The seed is the shape id, so a diagram looks the same on every render and every machine. Renderer only: the drawing format is unchanged, and switching back to clean (the default) is instant.

<MarkdownEditor value={value} onChange={setValue} drawingStyle="ink" />

Text switches to Recursive on its casual / cursive axes when the host page loads the font (it falls back to the regular face otherwise):

<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Recursive:slnt,wght,CASL,CRSV,MONO@-15..0,300..1000,0..1,0..1,0..1&display=swap" />

The geometry helpers (inkStroke, inkAmplitude, inkFillOffset, seedFrom, roundedRectPolygon, roundedPolyline) are exported for custom renderers.