D2 Component Library

Using the component library

Copy-and-adapt D2 templates for DSA and system design — the theme, the state classes, the two-panel transition, and the four layout rules that decide whether a figure lands.

Suggest an edit

Using the component library

The snippets lesson teaches the D2 language one keyword at a time. This chapter is the other half: finished figures to start from. An array strip with its indices, a sliding window mid-shrink, a cache-aside read path — drawn, tuned, and ready to be edited into whatever you actually need.

Nothing here is a screenshot. Every figure is drawn from the d2 fence printed above it, and every fence is generated from a real file on disk.

Where the sources live

synapse-features/_d2-blocks/
  lib/theme.d2         # every colour and size, in one place
  lib/icons.d2         # verified icon URLs for the system-design blocks
  lib/flatten.mjs      # resolves the imports away, for publication
  demo/                # the three figures on this page
  dsa/                 # data structures and algorithms
    *.anim/            # animations: a base diagram + one file per frame
  system-design/
    primitives/        # the boxes, grouped by tier
    shapes/            # where a native shape beats an icon
    boundaries/        # regions, AZs, VPCs, trust
    patterns/          # nine composed architectures
    animated/          # failover, growth, a deploy — and the four transports
  render.sh            # the gate: compile everything, twice, two engines
  README.md            # the index — every block, with a preview

The directory starts with _, which keeps the content walker out of it. Without the underscore its README.md would render as a lesson of its own.

Two vocabularies

Every block is written in two kinds of class. Structure says what a thing is — cell, idx, node, panel. State says what is happening to it. Layer them with the list form and recolouring becomes a one-word edit:

classes: {
  cell: { style: { fill: "#ffffff"; stroke: "#94a3b8"; font-size: 18 }; width: 64; height: 56 }
  idx: { style: { fill: transparent; stroke-width: 0; font-size: 13; font-color: "#64748b" }; width: 64; height: 24 }
  panel: { style: { fill: "#fbfcfe"; stroke: "#cbd5e1"; font-size: 15; font-color: "#475569" } }
  active:  { style: { fill: "#dcfce7"; stroke: "#16a34a" } }   # in play — inside the window, in range
  current: { style: { fill: "#fef08a"; stroke: "#ca8a04"; stroke-width: 3 } }   # being touched right now
  visited: { style: { fill: "#e2e8f0"; stroke: "#94a3b8"; font-color: "#64748b" } }   # already processed
  pruned:  { style: { fill: "#fee2e2"; stroke: "#dc2626"; stroke-dash: 4; font-color: "#991b1b" } }   # cut off, never explored
  hot:     { style: { fill: "#ffedd5"; stroke: "#ea580c" } }   # frequent — cache hit, high traffic
  cold:    { style: { fill: "#eff6ff"; stroke: "#93c5fd"; font-color: "#64748b" } }   # rare — cache miss, cold path
  dim:     { style: { fill: "#fbfcfe"; stroke: "#cbd5e1"; font-color: "#94a3b8" } }   # this frame is not about it
  idle:    { style: { fill: "#ffffff"; stroke: "#94a3b8" } }   # declared, untouched, recolourable
}
 
sw: "layer a state over a structure:  { class: [cell; current] }" { class: panel
  grid-rows: 2
  grid-columns: 9
  grid-gap: 0
 
  s0: "—" { class: cell }
  s1: "" { class: [cell; idle] }
  s2: "" { class: [cell; active] }
  s3: "" { class: [cell; current] }
  s4: "" { class: [cell; visited] }
  s5: "" { class: [cell; pruned] }
  s6: "" { class: [cell; hot] }
  s7: "" { class: [cell; cold] }
  s8: "" { class: [cell; dim] }
 
  n0: "(none)" { class: idx }
  n1: idle     { class: idx }
  n2: active   { class: idx }
  n3: current  { class: idx }
  n4: visited  { class: idx }
  n5: pruned   { class: idx }
  n6: hot      { class: idx }
  n7: cold     { class: idx }
  n8: dim      { class: idx }
}
layer a state over a structure: { class: [cell; current] }(none)idleactivecurrentvisitedprunedhotcolddim

Six states, and they are enough for everything in this chapter. active is in play, current is being touched right now, visited is done with, pruned was cut off, hot and cold are frequent and rare. If you find yourself reaching for a seventh, check whether one of these already means it — a consistent palette is worth more than a precise one.

The transition

This is the block to learn. Two panels — the state before a step and the state after it — joined by an arrow that names the step:

classes: {
  cell: { style: { fill: "#ffffff"; stroke: "#94a3b8"; font-size: 18 }; width: 64; height: 56 }
  idx: { style: { fill: transparent; stroke-width: 0; font-size: 13; font-color: "#64748b" }; width: 64; height: 24 }
  panel: { style: { fill: "#fbfcfe"; stroke: "#cbd5e1"; font-size: 15; font-color: "#475569" } }
  current: { style: { fill: "#fef08a"; stroke: "#ca8a04"; stroke-width: 3 } }   # being touched right now
  visited: { style: { fill: "#e2e8f0"; stroke: "#94a3b8"; font-color: "#64748b" } }   # already processed
  step: { style: { stroke: "#0f172a"; stroke-width: 2; font-size: 15; bold: true } }
}
 
direction: right
 
before: "before · writing index 3" { class: panel
  grid-rows: 2
  grid-columns: 6
  grid-gap: 0
 
  v0: 2 { class: [cell; visited] }
  v1: 7 { class: [cell; visited] }
  v2: 9 { class: [cell; visited] }
  # TODO: the cell about to change.
  v3: "·" { class: [cell; current] }
  v4: "·" { class: cell }
  v5: "·" { class: cell }
 
  i0: 0 { class: idx }
  i1: 1 { class: idx }
  i2: 2 { class: idx }
  i3: 3 { class: idx }
  i4: 4 { class: idx }
  i5: 5 { class: idx }
}
 
after: "after · index 3 written" { class: panel
  grid-rows: 2
  grid-columns: 6
  grid-gap: 0
 
  v0: 2 { class: [cell; visited] }
  v1: 7 { class: [cell; visited] }
  v2: 9 { class: [cell; visited] }
  v3: 4 { class: [cell; visited] }
  # TODO: and the one that becomes current next.
  v4: "·" { class: [cell; current] }
  v5: "·" { class: cell }
 
  i0: 0 { class: idx }
  i1: 1 { class: idx }
  i2: 2 { class: idx }
  i3: 3 { class: idx }
  i4: 4 { class: idx }
  i5: 5 { class: idx }
}
 
# Keep this label SHORT. ELK routes the edge straight through its text, so a long one
# ends up struck through by its own arrow. Two or three words, or a `\n` break.
before -> after: "write 4\nadvance" { class: step }
before · writing index 3after · index 3 written279···0123452794··012345 write 4advance

A single picture cannot show a change. Two can, and the arrow between them is where the reason goes. Nearly every -step file in this library is that skeleton with different cells inside:

direction: right
before: "before · <state>" { class: panel   <the figure> }
after:  "after · <state>"  { class: panel   <the figure> }
before -> after: "<the step>" { class: step }

Put the invariant in the panel titles — a running sum, a candidate count, a bound. The titles are what turn two pictures into an argument.

When two panels are not enough

The transition shows one step. For a sequence of them — a cache filling, a leader failing over, a window sliding — the library has animations: a base diagram plus one short file per frame, drawn into a figure the reader can play or step through. Animating a diagram covers the four ways to put one in a lesson and the rules that stop it jumping.

Copying one

The fence above is not byte-for-byte what is in dsa/array-strip-step.d2. The file starts with

...@../lib/theme

and a d2 fence has no filesystem, so that import would resolve to nothing. ./render.sh fence inlines the classes a block actually uses and prints the pair you see above:

./render.sh fence dsa/array-strip-step.d2

Copy either the fence off this page or the file off disk — the file if you want the theme, the fence if you want something self-contained.

Four rules that decide whether a figure lands

Set grid-rows and grid-columns. Not one of them. With only grid-columns, D2 fills column-major and re-flows the cells to fit, overriding the widths and heights you set:

classes: {
  cell: { style: { fill: "#ffffff"; stroke: "#94a3b8"; font-size: 18 }; width: 64; height: 56 }
  idx: { style: { fill: transparent; stroke-width: 0; font-size: 13; font-color: "#64748b" }; width: 64; height: 24 }
  panel: { style: { fill: "#fbfcfe"; stroke: "#cbd5e1"; font-size: 15; font-color: "#475569" } }
}
 
direction: down
 
wrong: "✗ grid-columns: 4 only" { class: panel
  grid-columns: 4
  grid-gap: 0
  v0: 2 { class: cell }
  v1: 7 { class: cell }
  v2: 9 { class: cell }
  v3: 4 { class: cell }
  i0: 0 { class: idx }
  i1: 1 { class: idx }
  i2: 2 { class: idx }
  i3: 3 { class: idx }
}
 
right: "✓ grid-rows: 2 AND grid-columns: 4" { class: panel
  grid-rows: 2
  grid-columns: 4
  grid-gap: 0
  v0: 2 { class: cell }
  v1: 7 { class: cell }
  v2: 9 { class: cell }
  v3: 4 { class: cell }
  i0: 0 { class: idx }
  i1: 1 { class: idx }
  i2: 2 { class: idx }
  i3: 3 { class: idx }
}
✗ grid-columns: 4 only✓ grid-rows: 2 AND grid-columns: 42794012327940123

That is the same eight cells in both panels. grid-gap: 0 is the other half — it is what makes cells read as one continuous strip instead of eight boxes.

Connections do not route into a grid. You cannot draw an arrow to cell 3. Use a spacer row instead: one invisible cell per column, and the caption goes in the column you mean.

classes: {
  cell: { style: { fill: "#ffffff"; stroke: "#94a3b8"; font-size: 18 }; width: 64; height: 56 }
  idx: { style: { fill: transparent; stroke-width: 0; font-size: 13; font-color: "#64748b" }; width: 64; height: 24 }
  ghost: { label: ""; style: { fill: transparent; stroke-width: 0 }; width: 64; height: 28 }
  tick: { style: { fill: transparent; stroke-width: 0; font-size: 14; font-color: "#0f172a" }; width: 64 }
  panel: { style: { fill: "#fbfcfe"; stroke: "#cbd5e1"; font-size: 15; font-color: "#475569" } }
  current: { style: { fill: "#fef08a"; stroke: "#ca8a04"; stroke-width: 3 } }   # being touched right now
}
 
t: "pointing at column 2" { class: panel
  grid-rows: 3
  grid-columns: 5
  grid-gap: 0
 
  v0: 2 { class: cell }
  v1: 7 { class: cell }
  v2: 9 { class: [cell; current] }
  v3: 4 { class: cell }
  v4: 1 { class: cell }
 
  i0: 0 { class: idx }
  i1: 1 { class: idx }
  i2: 2 { class: idx }
  i3: 3 { class: idx }
  i4: 4 { class: idx }
 
  # TODO: move the caption to whichever column you mean.
  p0 { class: ghost }
  p1 { class: ghost }
  p2: "▲ here" { class: tick; style.font-color: "#ca8a04" }
  p3 { class: ghost }
  p4 { class: ghost }
}
pointing at column 22794101234▲ here

ghost is blank padding; tick is the one that shows its label. Both are transparent and both pin their width to the cell width, so a long caption overhangs rather than knocking the columns out of line.

A nested container ignores direction. Set direction: down inside a panel and ELK will ignore it — every container lays out in the root's direction. So a two-panel figure holding trees needs direction: down at the root, and its panels stack vertically. That is why dsa/bst-step.d2 is the one transition in this chapter drawn top-to-bottom.

Siblings with no edge between them lay out perpendicular to the direction. direction: right stacks two unconnected panels; direction: down puts them in a row. Connect them with an edge and they line up along the direction instead. Counterintuitive, but consistent — and it is the knob to reach for when two panels land the wrong way round.

The gate

./render.sh all

Seven things, in order: every icon URL answers 200; every lib/ file parses; every block compiles and its flattened form compiles; the multi-board sources draw an animated SVG and a board tree; every highlight-mode animation is layout-stable to within 2%; the figures that ship as files are redrawn; and every fence in these lessons compiles again under the WASM d2 the app actually ships.

That last one is not paranoia. The app pins d2 v0.7.0 and renders with ELK at pad: 20; your CLI is probably v0.8.1 and defaults to dagre at pad: 100. Gate with the defaults and you are eyeballing a picture no reader ever sees. ./render.sh sheet puts a whole directory on one page so a collapsed cell has nowhere to hide.

Where next

Arrays, windows and pointers the strip, sliding windows, pointer pairs, prefix and difference arrays
Stacks, lists and hashing stacks, queues, deques, ring buffers, linked lists, hash tables, intervals
Trees, heaps and range structures binary trees, BSTs, heaps, tries, segment and Fenwick trees
Graphs and traversal three representations, BFS, DFS, topological order, union-find
DP and divide-and-conquer DP tables, recursion trees, binary search, partition, merge, grids
System design — primitives every box a system diagram needs, with verified icons and boundaries
System design — patterns nine composed architectures, trade-off included
Animating a diagram four transports for a sequence, and the rules that keep one watchable
Animated system design failover, growth, and a deploy that rolls back
Animated DSA a sliding window, a binary search, a BFS frontier, a DP table
Mark as read