Shapes and spacing
Snap to the grid
Rule. Every box corner, connection point and line segment lies on an 8px grid. Box sizes are multiples of 16px.
Why. A grid makes alignment the default instead of an accident: two boxes of one height in one rank share their top, centre and bottom, and parallel lines keep whole-step distances. It is the squared paper of hand drafting.
Source. Drafting practice; DECISIONS.md, “Format, look and layout control”.
Status: enforced. Placement snaps positions, slots and lanes snap in Slots and Routing. Reserved label boxes are the exception: their text is measured, so their edges need not lie on the grid.
Use one pen
Rule. All outlines and lines use one ink colour (#1f2933), one stroke width (1.25) and a white
fill. Dashes mean a dependency or comment, never emphasis. Secondary text, such as labels and
keywords, is smaller and grey. It is never a second weight of line.
Why. One weight keeps the drawing quiet. Differences then carry meaning: a dashed line is a different relation, not a different mood. Purchase et al. (2002) found that 61% preferred diagrams without font variation.
Source. Drafting custom, Tufte’s smallest effective difference (both via the research report); Purchase, Allder and Carrington 2002.
Status: enforced, by the shared stylesheet in core::svg.
Distinguish outlines from connectors
Rule. Outlines could be slightly heavier than connectors, at most 1:2 (for example 1.5 and 0.75), as ISO 128 separates thick and thin lines.
Why. The eye then separates things from relations at once.
Source. ISO 128-24 (via the research report, recommendation 6).
Status: planned. Not scheduled. It changes every golden file and needs the owner’s decision, since “one pen” is the current house style.
Size boxes to their text
Rule. A box is as wide as its name and keyword need, plus padding, and never smaller than the kind’s minimum (128 by 56 for a component, 96 by 40 for a state). Widths come from embedded Helvetica metrics, so a box is the same size on every machine.
Why. A box that fits its text reads as one unit. A fixed size either crowds long names or leaves short ones lost. Measured text keeps the layout deterministic.
Source. DECISIONS.md, “Font stack leads with Helvetica metrics”.
Status: enforced, by each adapter’s size function (leaf_size, state_size).
Leave room for what attaches
Rule. A side that carries several ends, ports or interface stubs is long enough to space them at least 24px apart, 16px clear of its corners. A state with many transitions is wider.
Why. Attachment points that crowd at a corner or touch each other make arrows unreadable, and force lines to jog to reach them.
Source. Drafting practice; Spread and order attachment points.
Status: enforced, by side_needs (component), slot_room (package) and the incident count in
state_size.
Keep gaps even and generous
Rule. Ranks are at least 64px apart and neighbours in a rank 40px apart. A gap grows when a label or interface needs it, and never shrinks below that.
Why. Even spacing reads as order. Generous gaps leave channels for lines to run in without crowding, which is cheaper than any later repair.
Source. Gansner et al. 1993 (separation in layered layout); drafting practice.
Status: enforced, by layered::Spacing and the edge gaps each adapter passes.
Grow a box rather than bend a line
Rule. When a connector jogs only because its two end boxes miss each other by a few grid steps, grow one box by up to two grid steps on a side, toward the other end, into free space, so that the line runs straight.
Why. Bends rank above uniform sizes (see When rules conflict). A box that is 16px wider costs almost nothing, while the jog it removes costs a turn in every reading of that line.
Good: Message Bus grows 8px at its top and bottom, and each target 8px toward it, so all three
connectors run straight. Before box growth, two of them jogged, because Message Bus was no taller
than Checkout and could not reach both rows.
diagram component "Lines straightened by a taller box"
component Checkout
component "Message Bus" as bus
component Payments
component Inventory
Checkout --> bus
bus --> Payments : OrderPlaced
bus --> Inventory : OrderPlaced
A box grows only into free space: 16px clear of every other box, inside its container’s padding, and clear of reserved labels, self-transition loops, interface stubs and ports. A box with stubs, ports or self-transitions keeps its size. Growth across the flow is tried before growth along it. Each growth is kept only when routing again shows the connector lost a bend, no total rose, and no two connectors came to leave one point together. Over the 103 golden files, box growth removes 80 bends after the corner inset and tab ends, and the two Compiler pipeline crossings. The bounds of the diagram do not change.
Source. Purchase et al. 2002 (bends 91%); Layout levers.
Status: enforced, by core::grow after layout, at up to two grid steps. Four steps, which the
study measured at another 38 bends, is not scheduled.
Keep a rank even
Rule. Boxes in one rank share a centre line along the flow, and should look like one row. When a box grows, prefer growth that keeps the row tidy, such as growing toward the gap between rows rather than away from it.
Why. Uneven heights make a row read as a staircase. This is the uniformity that box growth trades away, so it limits how far a box may grow.
Source. Drafting practice; the images in Layout levers.
Status: partial. Items in a rank are centred on one line (place_along). Box growth tries growth
across the flow first, which keeps a rank’s extent along the flow even, and grows at most 16px on a
side. A box may still end up a step taller or wider than its neighbours, as Kafka Adapter in
Hexagonal service or Mobile App in API gateway, and a box that grows on one side only has its name
up to 8px off its neighbours’ line.