Diagrams
DiagramExportMenu
Taking the figure off the page: PNG, JPEG, WebP, SVG and a 1200×630 share card.
Usage
When to reach for it
Ships from@misoto22/design/diagrams
import { DiagramExportMenu } from '@misoto22/design/diagrams'Notes
Taking the figure off the page: five files, one menu.
WHY THE MENU DOES THE WORK rather than handing back a format: every one of these exports is the same six steps — find the <svg>, walk it with getComputedStyle to bake the custom properties into real colours, serialise, rasterise, name the file, hand it to the browser — and only the last two differ between them. A menu that emitted 'png' and left the caller to do the rest would be a menu that every consumer reimplements, badly, and the interesting half (a serialised SVG resolves var(--ink) to nothing and comes out invisible) is exactly the half a caller would not know to write.
onExport is still there for a page with its own pipeline — a server-side renderer, a different frame size — and taking it turns everything below into a no-op, which is why a result from that path says source: 'caller': this component cannot see whether the handler produced a file, so it does not report that it did.
What each format actually is, stated rather than implied.
SVG is the artwork with resolved colours. It is editable and it is the only lossless one, but it carries no web fonts: a machine without the family renders it in a fallback, so type metrics will differ.
The ground is the reader's own surface unless background says otherwise. background={null} exports on transparency — for a figure being dropped onto a coloured page — except in JPEG, which has no alpha and is flattened onto paper whatever is asked for.
PNG, JPEG and WebP are the browser's own rasteriser re-drawing that SVG at 2×. Not a screenshot — antialiasing and any effect a page stylesheet applied from OUTSIDE the <svg> are not in it. JPEG has no alpha, so it is flattened onto the resolved paper colour rather than onto black, which is what a transparent PNG becomes when a format with no transparency is asked to hold it.
The share card is a 1200 × 630 frame with the title on it and the whole diagram letterboxed inside — never cropped. A card that cropped to fill the frame would be a picture of a different diagram.
Anatomy
| Element | Description |
|---|---|
| Triggerrequired | A small secondary button reading Export, or whatever trigger replaces it — the replacement is slotted, so it becomes the menu’s own control rather than sitting next to one. |
| Menurequired | A dropdown aligned to the trigger’s end, in three labelled groups separated by rules: Image, Vector, Share. The grouping is the answer to "which of these five do I want". |
| Format rowrequired | One menu row per format, with its name and a mono hint under it — "Lossless, 2× for retina", "Compact, flattened onto paper". Real menu rows, so the arrow keys walk them, typing jumps to one, and picking one closes the menu over the file it just wrote. The running one gains an ellipsis and every row is disabled until it finishes, so a second click cannot start a second export. |
| Serialiserrequired | The part with nothing to point at: the artwork is cloned, every node’s computed paint is written inline, and the clone is placed on a plate the size of the picture plus its padding. It is what stops var(--ink) arriving in a document with no stylesheet and painting nothing. |
| Share card | A fixed 1200 × 630 frame with the title printed on it and the whole diagram letterboxed inside — never cropped to fill, because a card cropped to fill is a picture of a different diagram. |
| Result | onResult, called with the format, an ok or the Error, and the source that produced it. It is the only place a failed export is reported. |
Best practices
Do
- Point targetRef at the wrapper rather than at an svg you found yourself. The export looks for the artwork marker first, which is what stops a page whose toolbar sits inside the same wrapper from exporting a picture of a chevron.
- Export after the artwork has been measured. The serialiser reads the element’s box and throws when it is still zero, so a call made in the same tick as the mount fails loudly instead of writing an empty file.
- Take onResult and put a failure in front of the reader. An unmeasured figure, a canvas tainted by a cross-origin image, a browser that returned no 2D context — each is reported there rather than thrown at the click, and the alternative is a menu item that quietly does nothing.
- Read source alongside ok. built-in says a file reached the browser; caller says your own onExport resolved, which is exactly what a handler that did nothing also does — the menu cannot see inside your pipeline and no longer reports as though it can.
- Pass background={null} for a figure going onto a coloured page. Every other export paints the reader’s own surface behind the artwork, and JPEG is flattened onto that surface whatever this says, because a transparent JPEG is a black one.
Don’t
- The file is the artwork and the theme the reader was in. Paint is read off the live element, so a figure exported from a dark page is a dark image in a light document; and the toolbar, the inspector and the figure’s own hidden summary list are HTML, so none of them travels with it.
- Do not treat the SVG as a pixel-exact record. The isolated document cannot fetch the page’s web fonts, so the type falls back to what the machine has — the words and the line breaks are already fixed, but a name that just fitted its plate on screen can overrun its own rule in the file.
- The output is sized from the artwork’s box ON SCREEN rather than from its viewBox, so a figure sitting at 4× inside a DiagramCanvas serialises four times larger and then rasterises at 2× on top of that. Reset the view before exporting, or a reader gets a file whose dimensions record where the zoom happened to be.
Examples
default
Five files behind one trigger, grouped by what they are for: PNG, JPEG and WebP are the artwork rasterised at 2x, SVG is the artwork itself with every custom property resolved to a real colour, and the share card is a 1200 x 630 PNG with the title printed above the diagram. targetRef points at the wrapper around the figure rather than at the svg, and the menu finds the figure’s own artwork inside it. title is what names the download, slugged — Request path arrives as request-path.png.
2 elements and 1 relationships.
- CloudFront (cloud) — CDN
- API (backend) — FastAPI
- CloudFront → API: HTTPS
what lands in the file
The ref here wraps the toolbar as well as the figure, and every icon in that toolbar is an svg of its own — the menu looks for the figure’s own artwork marker first, which is what stops an export of a diagram from being a picture of the info button. What leaves the page is the artwork alone: the toolbar, the panel and anything else drawn in HTML are not in the file. The colours are read off the live element, so the download comes out in whichever theme the reader is looking at, but web fonts do not travel with it and a machine without the family will set the labels in something else.
3 elements and 2 relationships.
- Checkout (backend) — Django
- Orders topic (messagebus) — Kafka
- Payments (external) — Stripe
- Checkout → Orders topic: publish
- Orders topic → Payments: capture
your own pipeline
onExport takes the whole job over: the serialise, rasterise and download steps never run, so nothing is written to disk unless this handler writes it. Reach for it when the file has to come from somewhere the browser cannot go — a server renderer, a frame size of your own, a queue. onResult still fires either way and is the seam a page raises a toast from; trigger replaces the button but not the menu, so an icon-only trigger still needs its aria-label.
2 elements and 1 relationships.
- CloudFront (cloud) — CDN
- API (backend) — FastAPI
- CloudFront → API: HTTPS
Parts
Composed at the call site rather than configured through props, so a layout this component did not anticipate is still expressible.
exportFigure
One figure, one format, one file.
Exported so a page can wire a keyboard shortcut or its own button to exactly what the menu does, without reimplementing the six steps.
Takes no props of its own.
Types
export type ExportFormat = 'png' | 'jpeg' | 'webp' | 'svg' | 'share-card'Accessibility
- A failed export is reported through onResult rather than swallowed: a click that quietly does nothing is indistinguishable from a broken button.