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 asdrawingToMermaid(data)). - Canvas: resizable height (bottom handle), dot grid, white surface
that inverts Excalidraw-style in dark mode. An optional
canvasWidthscales 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.
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:
| Field | Notes |
|---|---|
direction | "right" (default) or "down": the auto-layout flow |
canvasWidth, canvasHeight, width | Optional; 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"}
]}
```
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"}
]}
```
Top level
| Field | Type | Notes |
|---|---|---|
version | 2 | Literal. Any other version (or none) renders as an empty canvas |
canvasHeight | number | Canvas height in px (min 80; 300–400 is typical) |
canvasWidth | number | Optional logical width (min 120); the drawing scales to fit when set, fluid when absent |
width | "full" | "text" | "content" | Optional block width, default full |
shapes | Shape[] | 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.
| Type | Kind | Default size | Notes |
|---|---|---|---|
rect | box | 160 × 90 | The general-purpose card |
ellipse | box | 160 × 90 | Inscribed in the bounding box |
diamond | box | 170 × 100 | Decision; text area is the inner half |
note | box | 160 × 110 | Sticky note, folded corner; default fill #ffec99 |
cylinder | box | 140 × 110 | Database |
cloud | box | 180 × 110 | Outline is an ellipse for binding |
queue | box | 180 × 80 | Horizontal cylinder |
actor | box | 90 × 120 | Stick figure; text only, drawn under the figure |
arrow | connector | Head at the end; bidirectional for both | |
line | connector | No arrowhead | |
text | free text | Floating 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):
| Name | Stroke | Fill |
|---|---|---|
| plain | transparent | #f1f3f5 |
| black | #1e1e1e | #1e1e1e (text renders white) |
| gray | #1e1e1e | transparent |
| 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.