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

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 keywords top/right/ bottom/left work 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 share cy).
  • "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 when from === 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 }
  ]
}
The training loop: forward left-to-right, gradient flowing back.

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
}
AST for 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

  1. Add a case in drawBaseShape (napkin.js).
  2. Add a case in innerRect for text placement.
  3. Add a case in visualBounds if its visible edges differ from its bbox.

7.2 Adding a diagram type

  1. Write drawMyType(svg, cfg) somewhere in the file.
  2. Add an entry to DISPATCH mapping the type name to your function.
  3. Document the schema here.

7.3 Adding an arrow head style

  1. Add a branch in arrowHead for the new style.
  2. 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 semantic shorthand, per-shape innerRect and connectionPoint contracts, 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.