Diagram
A diagram from a few lines of text (Guest -> Portal: books): boxes, arrows, flows with things moving along them, groups, and step-through. Drawn at build time as a static SVG with its text alternative from the same source, so it reads and prints without JavaScript.
quiet warm playful
Open the live demo
npx susegad add folio-diagram
The prompt
the prompt
The sketch on the back of an envelope that explains how the thing works, made from a few lines of text.
Write Guest -> Portal: books a stay and get a diagram: hand-inked boxes and arrows that draw, flows with things moving along them, steps you can walk through, and the whole thing in words for anyone who cannot see it.
:::diagram{title="How a booking travels" steps}
Guest -> Portal: books a stay
Portal => Owner: asks to hold
Owner -> Guest: confirms
:::
The prompt
Make a diagram grammar for documents. Read a small line grammar: A -> B: label for an arrow, A => B for a flow, A <-> B both ways, A -- B a plain line, Name = Label, group Name: A, B, title:, direction: right|down, # comments, and > lines that give the arrow above them a step description in prose. Report bad lines with their line numbers and draw the rest. Lay it out in layers: rank each box by its longest path from where the story starts, breaking cycles where they close; order boxes within a rank by their neighbours' positions, keeping groups together; join facing sides with cubic curves; loop arrows that go back beyond the boxes, and send arrows that skip a rank over the top. Keep boxes that are not in a group out of its frame. Work in rank and cross coordinates so the same code lays out rightward and downward. Render it as a string of SVG with no DOM, so a document build can seal it: classes and presentation attributes only, never inline styles. Make every diagram carry its text alternative from the same model: the parts and connections behind "Read the diagram as text", or the steps as an ordered list. Give it three registers. Quiet: hairline boxes and plain arrows. Warm: boxes drawn in four inked strokes that cross at the corners, and lines inked with the engine's wobble and pressure, returned as SVG outlines rather than painted. Playful: the same, with boxes tinted from the palette and flows in the accent colour. In the page, draw the arrows in once when the diagram comes into view with Web Animations on a stroke dash (through a mask for the inked ones), move dots along flows and pause them off screen, and add step-through with buttons, arrow keys and a polite live region that says each step. Leave flows as still dots in quiet and under reduced motion. Draw again if the register changes, and turn a rightward diagram downward on a narrow screen unless its author chose the direction.
Words to code
| When you say | Technique | What happens |
|---|---|---|
| a small line grammar | parsing | parse(src) reads line by line with a handful of patterns. Arrows are tried before Name = Label, because => contains =. A > line joins the arrow above it while only descriptions come between. |
| rank each box by its longest path … breaking cycles where they close | layout | A depth-first walk from the sources, then from the order boxes first send an arrow, marks the edges that close a cycle. The others push each box's rank to one more than its predecessor's. |
| order boxes … keeping groups together | layout | Six sweeps sort each rank by the mean position of its neighbours in other ranks. Grouped boxes sort by their group's mean, so they stay side by side. |
| loop arrows that go back … over the top | layout | Back edges become a cubic curve out beyond the boxes, and beyond the labels on that side. Forward edges that would pass through a box in a middle rank arc over all the boxes instead. |
| rank and cross coordinates | layout | Everything is placed in (u, v): u along the ranks, v across them. Only at the end is (u, v) mapped to (x, y) or (y, x). |
| a string of SVG with no DOM | sealing | renderDiagram builds markup by hand and escapes every value. It uses no style attributes, so the Folio seal's Content-Security-Policy needs no extra hashes. |
| its text alternative from the same model | accessibility | describe(model) writes the summary, the parts (with their groups) and every connection ("Booking portal sends to Owner: asks to hold."). The SVG is role="img", named by its <title> and described by that list. |
| four inked strokes that cross at the corners | ink | inkBox jitters the corners, bows each side a little and runs every stroke 2 to 5 units past its corner. inkPath is the engine's ink() recipe (resample, sideways noise wobble, breathing width, tapered ends), returned as the ribbon's outline. |
| draw the arrows in … through a mask | motion | The plain centreline carries a stroke dash animated from its length to 0. For inked lines, a white stroke along the centreline in a <mask> reveals the ribbon. Each animation is cancelled when it finishes. |
| move dots along flows and pause them off screen | motion | Each dot is a <circle> animated through 25 translate() keyframes sampled with getPointAtLength, looping, spread out by negative delays. An IntersectionObserver pauses and plays them. |
| a polite live region that says each step | accessibility | A role="status" line reads "Step 2 of 4." and the step's words. The list item gets aria-current="step". The lines that are not current fade back, but their words stay in the soft text colour. |
| turn a rightward diagram downward on a narrow screen | layout | A ResizeObserver compares the element's width with the drawn width. Below about 72% it renders again with direction: down, unless the source or the options chose a direction. |
Credit
Grown from the Susegad engine's hand-inked line and from the old habit of explaining how something works by drawing boxes and arrows on whatever paper is to hand. Tier: pan-Indian.
A few lines of text become a diagram: boxes, arrows that draw, flows with things moving along them, and step-through. It is drawn when the document is built, so a sealed Folio document carries it as plain SVG that reads and prints without JavaScript. Every diagram has a text alternative made from the same source.
Write one
title: How a booking travels
Portal = Booking portal
group Casa Exemplo: Owner, Caretaker
Guest -> Portal: books a stay
> The guest picks the dates and pays a deposit on the booking portal.
Portal => Owner: asks to hold
Owner -> Caretaker: readies the house
Owner -> Guest: confirms
| Line | Means |
|---|---|
A -> B: label | an arrow from A to B, with an optional label |
A => B: label | a flow: an arrow with things moving along it |
A <-> B: label | both ways |
A -- B | a plain line |
> words | prose for the arrow just above: its step, read out and listed. Several > lines join up. |
Name = Label | show a node under a longer label |
group Name: A, B, C | a frame round A, B and C, named. A node is in one group at most. |
title: … | the caption, and the name of the picture for screen readers |
direction: right or down | which way the diagram runs. Default: right, turning down on a narrow screen. |
# … | a comment |
Names are what you type (quotes are optional: "Night watch" -> Owner). A line it cannot read is reported with its line number, and the rest is still drawn.
In a Folio document
:::diagram{title="How a booking travels" steps}
Guest -> Portal: books a stay
…
:::
The attributes are the options below; bare steps means true. (The directive syntax belongs to packages/folio/md.)
From code
import { renderDiagram } from './render.js'; // pure, synchronous, Node or browser
const { html, text, errors } = renderDiagram(src, { title, direction, register, steps, id, seed, lang });
| Option | Values | Default |
|---|---|---|
id | a stable id for the figure | a hash of the source |
title | overrides title: in the source | |
direction | right, down. Chosen here or in the source, it is kept on every screen. | from the source, else right |
register | quiet, warm, playful: the look drawn into the static SVG | warm |
steps | add step-through, and list the steps | false |
seed | fixes the hand-drawn wobble | the id |
lang | the figure's lang |
html:<sg-diagram>holding a<figure>: caption, SVG, and the text alternative. It uses classes and presentation attributes only, neverstyle="".text: the text alternative as plain text.errors:[{ line, message }], line numbers counted from the first line ofsrc. A line the grammar cannot read is reported and left out; the rest still draws. A line with an arrow mark that is not a whole connection (A ->,-> B,A -->) or with two arrows (A -> B -> C) is an error, never a box.renderDiagramdoes not throw on a bad source.
The page needs diagram.css, and diagram.js for the live parts. Both are only needed once per page.
In the page (diagram.js)
- Arrows draw one after another the first time the diagram comes into view. In warm and playful the inked line is revealed through a mask on a stroke dash, and in quiet the line itself draws. These are Web Animations, cancelled once they finish.
- Flows move: dots travel along each
=>arrow, three in warm and five in playful. They pause off screen. In quiet and under reduced motion they stay as three still dots, which is also what the static SVG and print show. - Step-through (with
steps): Previous step and Next step buttons, with the arrow keys, Home and End while focus is on them. The current step's arrow and its two ends stay sharp. The other lines fade back, but their words stay readable. A polite status line says "Step 2 of 4." and the step's words, and the list marks the step witharia-current="step". At either end the button says it is unavailable (aria-disabled) but keeps focus.element.stepgets or sets the step (0 shows all of them). Thesg-diagram-stepevent carries{ step, of }. - Register: if the page's register differs from the one drawn at build time, or changes later, it draws again from the same source and keeps the step.
- Narrow screens: a diagram that runs right, where the author did not choose a direction, draws again running down when it would otherwise shrink below about three quarters of its size.
Registers
| Boxes and lines | Motion | |
|---|---|---|
| quiet | hairline boxes, plain lines and arrowheads, a dashed frame for groups | none beyond what a step needs (180 ms) |
| warm | boxes drawn in four inked strokes that cross at the corners, inked lines with the engine's wobble and pressure, labels and group names in the hand face | arrows draw once; flows move |
| playful | as warm, with boxes and groups tinted from the palette and flows in the accent colour | as warm, faster and fuller |
Accessibility
- The SVG is
role="img", named by its title and described by the text alternative: the steps as an ordered list when there are steps, otherwise the parts and every connection behind "Read the diagram as text". Both come from the same model as the picture. - Every step is readable without JavaScript. Step-through only adds a second way in.
- Words in the diagram use token text colours. Axe cannot measure SVG text, so contrast is checked by hand: box words are
--sg-texton--sg-surface-raised; labels are--sg-text-softwith a halo in the surface colour; while stepping, the words of parts that are not current turn--sg-text-softinstead of fading. - Buttons are real
<button>s. Focus never drops.
In a sealed document
No inline styles, no network, no eval. Built into a sealed Folio file, the demo made no requests and no policy violations and needed no runtime style hashes. Stepping and a register change also worked inside the sealed file, with the network blocked.
Known limits
- The layout is layered and simple. Large or tangled graphs will cross lines. Aim for up to a dozen boxes.
- Text widths are estimated in Node, not measured, so an unusual font can make a box a little wide or tight.
- A group's frame is a rectangle round its members. Outsiders in the ranks it spans are pushed clear, but groups that interleave across ranks can still overlap each other.
- Without JavaScript on a phone, a wide diagram scales down. Its text alternative is always readable.