napkin.js
A declarative DSL for hand-drawn diagrams in posts. Write JSON inside a
figure element; the runtime parses it and renders inline SVG via
rough.js, set in
Excalifont.
This page is the full reference. Long, scannable, every shape and every diagram type has at least one rendered example.
Table of contents
- 1. Overview
- 2. Concepts
- 3. Shapes — geometric, tech, ml/math, modifiers
- 4. Edges — heads/tails, styles, routing, labels, semantic
- 5. Diagram types
- 6. Composition
- 7. Extending napkin.js
- 8. API reference
- 9. Changelog
1. Overview
1.1 What this is
napkin.js is the rendering engine behind every hand-drawn illustration
on this site. It turns a compact JSON description into a real SVG with
wobbly lines and a paper-and-ink aesthetic. Roughly 55 shapes and 27
diagram types are built in.
1.2 Hello world
The shortest possible diagram:
<figure class="diagram" data-napkin>
<script type="application/json">
{ "type": "flow", "nodes": [
{ "id": "a", "label": "hello" },
{ "id": "b", "label": "world" }
], "edges": [ { "from": "a", "to": "b" } ] }
</script>
</figure>
1.3 The figure wrapper
Every diagram lives inside a <figure class="diagram" data-napkin>
element with a single <script type="application/json"> child. The
runtime walks the page on DOMContentLoaded, parses each script’s
contents, and prepends an <svg class="napkin-svg"> to the figure.
<figure class="diagram" data-napkin>
<script type="application/json">
{ "type": "...", ... }
</script>
<figcaption>Optional caption.</figcaption>
</figure>
The figure.diagram[data-napkin] selector is the public surface; the
JSON schema inside is documented below.
2. Concepts
2.1 Nodes and edges
Most diagrams are a list of nodes and a list of edges. Every node
has an id (unique within the diagram) and a label. Every edge
references a from and to id.
2.2 Auto-layout vs manual
By default, nodes flow left-to-right at equal vertical centers. Set
explicit x and y on a node to break out of auto-layout for that one
node; the cursor advances past it based on its width.
2.3 Connection points
Edges snap to the boundary of their endpoint shapes. from: "a"
chooses the nearest side along the line to b. To pin an endpoint
explicitly, append .side — "a.right" snaps to A’s right midpoint;
sides are top, right, bottom, left, plus the four corners. For
ellipse and circle shapes, "a.t=0.7" snaps to 70% around the
circumference.
2.4 Text fitting
All shapes auto-size to fit their label. A diamond carrying a 20-char
label sizes up so its inscribed rectangle has room. Manual width and
height always win.
2.5 Themes
Colors pull from the site’s “warm paper” palette, with a dark warm
variant under data-theme="dark" on the root element. Inside a config,
the keyword "accent" resolves to the warm-red highlight; "ink" /
"paper" / "muted" are also available; anything else is a literal
CSS color.
3. Shapes
A shape is what a node looks like. There are three families: geometric (the universal vocabulary), tech components (icons with a role), and ml/math (matrices, neurons, attention heads).
Every shape implements two contracts:
innerRect— the largest axis-aligned rectangle where text fits.connectionPoint— where edges attach. Side keywordstop/right/bottom/leftwork for every shape.
3.1 Geometric primitives
Fourteen base shapes. The defaults are tuned so that putting any text inside any shape “just works” — auto-sizing inflates the bbox so the inscribed rectangle is wide enough.
Each accepts the property modifiers in §3.4. For example, all five
“rounded-friendly” shapes with rounded: true:
3.2 Tech components
Higher-level shapes for system and architecture diagrams. Each is a base geometric primitive plus a small decorative glyph or “tech feel.” Use them when the role is the point, not the geometry.
3.3 ML / math icons
For neural-net and tensor diagrams.
3.4 Property modifiers
These turn one shape into many without proliferating shape names:
| Property | Effect |
|---|---|
rounded: true |
Soften corners on rect/diamond/triangle/hexagon/parallelogram |
fill: "accent" |
Fill in the accent color instead of paper |
fill: "muted" |
Slightly darker than paper |
fill: "<css color>" |
Arbitrary color (e.g. "#e5d4ad") |
stroke: "thin" |
1.4px stroke |
stroke: "thick" |
3.0px stroke |
style: "dashed" |
Dashed border |
style: "dotted" |
Dotted border |
badge: "..." |
Small label in the top-right corner |
points: N |
(star) number of points |
direction: ... |
(triangle / block-arrow) facing |
flat: true |
(hexagon) flat-top variant |
flip: true |
(trapezoid) mirror vertically |
Fills:
Strokes and styles:
Badges, points, direction, flat, flip:
4. Edges
One edge type, many properties. Defaults: head "open", tail "none",
style "solid", route picked by geometry.
interface Edge {
from: string; // "id" or "id.side" or "id.t=0.7"
to: string;
label?: string;
head?: ArrowEnd; // default "open"
tail?: ArrowEnd; // default "none"
style?: LineStyle; // "solid" (default) | "dashed" | "dotted"
route?: EdgeRoute; // "straight" | "elbow" | "curve" | "back" | "self"
color?: Color; // "ink" (default) | "accent" | CSS color
thickness?: "thin" | "normal" | "thick";
gap?: boolean; // default true; label masks the line behind it
semantic?: "implements" | "extends" | "consumes" | "produces"
| "uses" | "owns" | "depends";
back?: boolean; // shorthand for route: "back"
}
type ArrowEnd =
| "open" | "triangle" | "triangle-open" | "filled"
| "bar" | "dot" | "circle" | "diamond" | "diamond-filled" | "none";
4.1 Heads and tails
Every head / tail style on one row:
Tails are the same vocabulary, used at the source end:
4.2 Line styles
4.3 Routing
"straight"— direct line (default when source and target sharecy)."elbow"— horizontal-vertical-horizontal step (default when they don’t)."curve"— smooth cubic bezier."back"— arcs below the row in accent red. Shorthand:back: true."self"— loop above the node. Implicit whenfrom === to.
4.4 Labels with gap
By default an edge label masks the line behind it (a paper-colored rect
underneath the text), so the line appears to break around the label.
Set gap: false to disable.
4.5 Semantic edges
Shorthand semantic field for the seven common UML/architecture edge
shapes. Each pre-fills head, tail, and style:
| Semantic | Head | Style |
|---|---|---|
implements |
triangle-open | dashed |
extends |
triangle-open | solid |
consumes |
open | dashed |
produces |
open | solid |
uses |
open | dashed |
owns |
diamond-filled | solid |
depends |
open | dashed |
5. Diagram types
For each type: short description, TypeScript-style schema, at least one rendered example. Types with aliases are noted in parentheses.
5.1 flow (graph)
Auto-laid-out boxes connected by arrows. Most general-purpose diagram type; the backbone of flowcharts, pipelines, and architecture drawings.
interface FlowConfig {
type: "flow" | "graph";
nodes: Node[];
edges: Edge[];
nodeWidth?: number; nodeHeight?: number;
gapX?: number; backDip?: number;
}
{
"type": "flow",
"nodes": [
{ "id": "d", "label": "data", "shape": "ellipse" },
{ "id": "m", "label": "model\nfθ" },
{ "id": "p", "label": "ŷ" },
{ "id": "l", "label": "loss" }
],
"edges": [
{ "from": "d", "to": "m" },
{ "from": "m", "to": "p" },
{ "from": "p", "to": "l" },
{ "from": "l", "to": "m", "label": "∂ℓ/∂θ", "back": true }
]
}
5.2 tree (mindmap)
Recursive top-down or left-right tree. Auto-spaced leaves; parents
centered above their children. mindmap = tree with direction: "LR".
interface TreeConfig {
type: "tree" | "mindmap";
root: TreeNode;
shape?: Shape;
direction?: "TB" | "LR";
}
interface TreeNode extends Node {
children?: TreeNode[]; // alias: branches
}
x*2 + (y − 3).5.3 states (fsm)
Pill-shaped states with labeled transitions. from === to creates a
self-loop; curve: "below" arcs the edge under the row.
interface StatesConfig {
type: "states" | "fsm";
states: State[];
transitions: Transition[];
}
interface State extends Node {
initial?: boolean;
}
interface Transition {
from: string; to: string; label?: string;
curve?: "below";
}
5.4 class (uml)
UML class diagram with name / attributes / methods sections plus inheritance arrows.
interface ClassConfig {
type: "class" | "uml";
classes: ClassNode[];
}
interface ClassNode {
id: string;
name: string;
abstract?: boolean;
extends?: string;
attrs?: string[];
methods?: string[];
x?: number; y?: number;
}
5.5 er
Entity-Relationship diagram. Entities are boxes with attribute lists; relationship edges may carry cardinality annotations.
interface ERConfig {
type: "er";
entities: Entity[];
relationships: Relationship[];
}
interface Entity {
id: string; name?: string;
fields?: (string | { name: string, pk?: boolean })[];
x?: number; y?: number;
}
interface Relationship {
from: string; to: string;
label?: string;
fromCard?: string; toCard?: string;
}
5.6 seq (sequence)
Actors with vertical lifelines and messages between them.
return: true produces a dashed accent-red return arrow.
interface SeqConfig {
type: "seq" | "sequence";
actors: string[];
messages: SeqMessage[];
}
interface SeqMessage {
from: string; to: string; label?: string;
return?: boolean;
}
5.7 swimlane
A flow split into named horizontal lanes. Each lane has its own nodes.
interface SwimlaneConfig {
type: "swimlane";
lanes: { label: string, nodes: Node[] }[];
edges: Edge[];
}
5.8 array (stack)
Annotated cells with pointer callouts and an optional bracket. The Crafting Interpreters call-frame diagram.
interface ArrayConfig {
type: "array" | "stack";
cells: string[];
indices?: boolean;
pointers?: { label: string, at: number }[];
bracket?: { label: string, from: number, to: number };
}
5.9 memory (struct)
Struct-style boxes with named fields. Fields with a ref property
render in red and emit a curved pointer arrow to their target block.
interface MemoryConfig {
type: "memory" | "struct";
blocks: MemoryBlock[];
}
interface MemoryBlock {
id: string; title?: string;
fields: { key: string, value: string, ref?: string }[];
x?: number; y?: number; width?: number;
}
5.10 timeline
Events on a horizontal time axis. Events alternate above and below.
interface TimelineConfig {
type: "timeline";
events: { at: number, label: string, note?: string }[];
markers?: number[];
labelTop?: string;
}
5.11 gantt
Tasks on a time axis with dependency arrows.
interface GanttConfig {
type: "gantt";
tasks: { id?: string, label: string, start: number,
duration: number, color?: Color }[];
deps?: { from: string, to: string }[];
}
5.12 cluster
A labelled box that groups other shapes. Useful for marking subsystems or VPCs inside a larger architecture diagram.
interface ClusterConfig {
type: "cluster";
clusters: { label?: string, x?: number, y?: number, gap?: number,
fill?: Color, nodes: Node[] }[];
nodes?: Node[]; // external nodes outside any cluster
edges?: Edge[];
}
5.13 bars (barchart)
Horizontal or vertical bar chart.
interface BarsConfig {
type: "bars" | "barchart";
bars: { label: string, value: number, color?: Color }[];
orientation?: "horizontal" | "vertical";
title?: string; xLabel?: string;
}
5.14 pie
Proportional slices with auto-positioned outside labels.
interface PieConfig {
type: "pie";
slices: { label: string, value: number, color?: Color }[];
radius?: number;
}
5.15 venn
2- or 3-set Venn diagram. Region labels are keyed by ∩ (the math
intersection symbol): "A", "A∩B", "A∩B∩C".
interface VennConfig {
type: "venn";
sets: { label: string }[];
regions?: { [key: string]: string };
}
5.16 linechart (lineplot)
Multi-series line chart with smooth curves through the data.
interface LineChartConfig {
type: "linechart" | "lineplot";
series: { label: string, points: [number, number][], color?: Color }[];
title?: string; xLabel?: string; yLabel?: string;
}
5.17 scatter
2D scatter plot with multiple series and a legend.
interface ScatterConfig {
type: "scatter";
series: { label: string, points: [number, number][], color?: Color }[];
}
5.18 heatmap
Cells colored by value. Useful for confusion matrices.
interface HeatmapConfig {
type: "heatmap";
values: number[][];
rowLabels?: string[]; colLabels?: string[];
title?: string; showValues?: boolean;
}
5.19 histogram
Binned counts.
interface HistogramConfig {
type: "histogram";
bins: { label?: string | number, count: number, color?: Color }[];
title?: string; xLabel?: string; yLabel?: string;
}
5.20 radar (spider)
Radial multi-axis plot.
interface RadarConfig {
type: "radar" | "spider";
axes: string[];
series: { label: string, values: number[], color?: Color }[];
max?: number; rings?: number;
}
5.21 area
Line chart with the region below the curve filled.
interface AreaConfig {
type: "area";
series: { label: string, points: [number, number][], color?: Color }[];
}
5.22 network (mlp)
Stacked layers of neurons, fully connected by default.
interface NetworkConfig {
type: "network" | "mlp";
layers: { label?: string, size: number }[];
connections?: boolean;
}
5.23 attention
Attention-weight matrix between tokens; optional top-K arrows.
interface AttentionConfig {
type: "attention";
tokens: string[];
weights: number[][]; // n × n in [0, 1]
title?: string; topK?: number;
}
5.24 conv
Convolution diagram: input ⊗ kernel = output.
interface ConvConfig {
type: "conv";
input: [number, number]; // [rows, cols]
kernel: [number, number];
output?: [number, number]; // inferred if omitted
cellSize?: number;
}
5.25 pipeline
A flow with defaults tuned for ML data pipelines. Identical schema to
flow; use it as a semantic hint that the diagram is ML/data-flow.
5.26 grid (matrix-diagram)
Generic 2D matrix with optional row+column labels and highlighted cells.
interface GridConfig {
type: "grid" | "matrix" | "matrix-diagram";
cells: string[][];
rowLabels?: string[]; colLabels?: string[];
highlight?: [number, number][];
title?: string;
}
5.27 axes (plot)
2D coordinate plane with vectors and labelled points.
interface AxesConfig {
type: "axes" | "plot";
xRange?: [number, number]; yRange?: [number, number];
xLabel?: string; yLabel?: string;
vectors?: { from?: [number,number], to: [number,number],
label?: string, color?: Color }[];
points?: { at: [number, number], label?: string }[];
grid?: boolean; size?: number;
}
5.28 architecture
A flow with defaults tuned for system diagrams (tech-component shapes,
elbow routing). Same schema as flow; this is a semantic hint.
6. Composition
6.1 Multiple diagrams in one post
Stack figure.diagram blocks vertically. They render in document order.
Use <figcaption> to tie them together narratively.
6.2 Cluster as a container
Use the cluster type when you want to mark “this group of nodes is a
subsystem.” Cluster boxes render dashed and sit underneath their
contents.
6.3 Escape hatch: paste Excalidraw SVG
When a diagram needs something the DSL can’t express (annotated brackets
across nested things, freehand sketches), draw it in
Excalidraw, use Export → SVG with “Embed
scene” enabled, and paste the SVG into a
<figure class="diagram"> ... </figure>. The paper-card frame and
Excalifont typography apply automatically.
7. Extending napkin.js
Every primitive lives behind a drawXxx(svg, cfg) function in
assets/js/napkin.js.
7.1 Adding a shape
- Add a
caseindrawBaseShape(napkin.js). - Add a
caseininnerRectfor text placement. - Add a
caseinvisualBoundsif its visible edges differ from its bbox.
7.2 Adding a diagram type
- Write
drawMyType(svg, cfg)somewhere in the file. - Add an entry to
DISPATCHmapping the type name to your function. - Document the schema here.
7.3 Adding an arrow head style
- Add a branch in
arrowHeadfor the new style. - Document it under §4.1.
8. API reference
The helpers visualBounds, autoSizeNode, drawShape, innerRect,
connectionPoint, drawLabel, drawEdge, text, and arrowHead
cover most of what new renderers need. Each is documented with Google-
style JSDoc in
assets/js/napkin.js.
9. Changelog
- v2 — full shape vocabulary (~55 shapes), unified Edge type with
UML-style
semanticshorthand, per-shapeinnerRectandconnectionPointcontracts, new diagram types: histogram, radar, area, attention, conv, pipeline, gantt, er, swimlane, cluster, architecture. Backward compatible with v1 schema. - v1 — 18 shapes; flow, tree, mindmap, states, class, seq, array, memory, timeline, bars, pie, venn, linechart, scatter, heatmap, network, grid, axes.