Skip to content

Shapes

A shape is a parametric graphic: an outline whose every coordinate is an affine function of the element's box (w, h) and its cornerRounding style (r). A single shape element replaces the old rect and ellipse primitives — the outline is described by a path, so any silhouette is possible, and an optional stroke-only decoration adds the internal detail of a compound shape (a database rim, a note crease).

Shapes lay out like the shapes they replace: size the element (via a layout block or a width/height style) and the outline fills it, or add content and the shape grows to fit it.

Both directions are solved for the outline as it actually is, never for its bounding box. Given a size, the content goes into the largest-area rectangle that fits inside the outline, clear of the stroke and of any decoration. Given content, the shape becomes the smallest one — by area — that still holds it, which is why an ellipse settles around √2 times its content, a diamond around twice it, and a chevron stays exactly as flat as its content: every bit of extra height would be paid for twice over in width by its point and notch.

To fill a shape with the diagram background (so it stays readable in both light and dark themes) use the background style variable rather than a fixed color like "white". The examples below set it once per diagram via type("shape") { fill = var("background") }.

In every example the shape wraps a thin dashed rectangle with no size of its own, so it expands to fill the content region — showing exactly where and how large the fitted content box is. The size is given to the surrounding element with a layout block, not to the shape itself.

Predefined shapes

The defaultShapes object is available in every diagram and provides ready-made shape definitions. Pass one as the shape attribute: shape(shape = defaultShapes.<name>). Available shapes: rect, ellipse, circle, diamond, hexagon, parallelogram, note, box3d, database, chevron, chevronStart, and chevronEnd.

Each shape below is shown once, with the dashed content box filling it. Where a shape reacts to the cornerRounding style (currently the rectangle), a rounded variant is shown as well.

Rectangle

The rectangle honours the cornerRounding style; the rounded corners collapse to sharp when it is 0 (the default).

Ellipse

ellipse fills the box; the content is fitted to the curve, so it never pokes out of the sides.

Circle

circle is the same outline as ellipse, meant for an equal width/height.

Diamond

Useful for decisions and gateways. The content box is the largest rectangle inscribed in the slanted outline.

Hexagon

A six-sided shape, often used for a process or preparation step.

Parallelogram

A slanted box, commonly used for input and output.

Note

A rectangle with a folded corner; the fold crease is drawn as a decoration. The fold has a fixed size of 20 pixels instead of scaling with the box, so a note stays a note at every size — which also means the shape only makes sense from about twice that in each direction.

3D box

A box drawn in perspective, the notation UML uses for a node. Its two hidden edges are a decoration, which is what keeps the content inside the front face: the content box is the largest rectangle clear of every stroke, and the front face is exactly that. Like the note's fold, the depth is a fixed 20 pixels rather than a share of the box, so the shape only makes sense from about twice that in each direction.

Database

A cylinder whose front rim is a decoration; the content is placed below the rim automatically.

Chevron

chevron carries a point on the right and a matching notch on the left, so a row of them interlocks into a process chain. chevronStart has only the point (its left edge stays flat, for the first block of such a chain), chevronEnd only the notch (its right edge stays flat, for the last one). Point and notch are always h/2 deep, which makes the blocks fit together at any height.

Padding

A shape keeps its content off its own border with the padding style, a single number inset on all four sides of the fitted content box. It is a property of the shape rather than of what is in it, so the shape grows by it and every child stays clear of the outline without any of them saying so.

padding is not specific to shapes — every element that lays out contents takes it, so a container, a canvasElement and a marker inset theirs the same way. See the DSL reference for how it differs from a margin.

Dividers

A divider is a rule drawn across the interior of a shape. It is not a path that happens to look like a line: it reaches the shape's border exactly, whatever that border does there, and it breaks wherever a decoration is in its way.

Three things about it are decided for you rather than declared:

  • Where it may appear. A divider is only valid as a direct child of a shape. Nothing else accepts one — not even a container inside a shape — so putting one anywhere else is an error the type system reports, not something that renders oddly.
  • Which way it runs. It follows the flow of the shape it sits in: horizontal in a vbox, vertical in an hbox. The two panels above differ only in the shape's layout. A stack has no flow, so a divider in one is an error.
  • How far it reaches. To the outline, and to any decoration it meets on the way — the database above is one shape, and its rim stops the rule exactly as the border does. A shape whose interior falls apart at that height gets one rule per piece, and where the border bites into the rule rather than merely ending it — a thick rule sitting on a chevron's notch — the rule takes the bite.

Padding does not apply to it. Content is held off the border, a divider bleeds to it; that is the point of the two being separate.

A divider is a stroke, not a box. It takes the stroke* styles, the margin* styles and visibility, and nothing else: it has no fill, and no size, alignment or flex properties, because it has no size of its own to give — it spans whatever the shape leaves it, and contributes nothing to how wide or tall the shape has to be. strokeLineJoin, strokeLineCap and strokeMiterLimit are not among the styles it takes either: a rule is straight, so it has no corner to join, and it ends where the shape's border does, which leaves a cap nowhere to go.

The gallery has a divider in every predefined shape, drawn in a contrasting colour and with a heavy stroke, which is the honest way to look at a termination: a rule painted like its outline hides exactly the gap or overlap one is looking for.

Custom shapes

A shape definition is an object with a path and an optional decoration, both SVG-like path strings. Every coordinate, Bézier control and arc radius is an affine expression over the box width w, the box height h, and the corner rounding r (+ - * /, parentheses, and implicit multiplication like 0.5h); the arc rotation and flags are plain constants. Supported commands are M, L, H, V, C (cubic Bézier), Q (quadratic Bézier), A (elliptical arc) and Z.

The outline is stroked and filled; the decoration is only ever stroked. A segment of zero length is dropped, so a corner drawn with an r-radius arc simply disappears when cornerRounding is 0.

A tag/label with a rounded end whose radius follows cornerRounding:

A document — the classic flowchart symbol — whose wavy lower edge is a single cubic Bézier (C). The content box is fitted above the wave, clear of the curve: