Skip to main content

Nodes & Shapes

This guide covers creating nodes, choosing shapes, styling them, and embedding media.

VizCraft ships with 22 built-in shapes plus a custom-path escape hatch. Here they are at a glance:

Shape reference

ShapeBuilder methodParameters
Circle.circle(r)radius
Rectangle.rect(w, h, rx?)width, height, corner radius
Diamond.diamond(w, h)width, height
Cylinder.cylinder(w, h, arcHeight?)database symbol
Hexagon.hexagon(r, orientation?)'pointy' (default) or 'flat'
Ellipse.ellipse(rx, ry)radii
Arc.arc(r, startAngle, endAngle, closed?)pie slice
Block Arrow.blockArrow(len, bodyW, headW, headLen, dir?)directional arrow
Callout.callout(w, h, opts?)speech bubble with pointer
Cloud.cloud(w, h)thought bubble
Cross.cross(size, barWidth?)plus sign
Cube.cube(w, h, depth?)3D isometric
Document.document(w, h, waveHeight?)wavy bottom
Note.note(w, h, foldSize?)folded corner
Parallelogram.parallelogram(w, h, skew?)I/O symbol
Star.star(points, outerR, innerR?)star/badge
Trapezoid.trapezoid(topW, bottomW, h)manual operation
Triangle.triangle(w, h, direction?)'up'/'down'/'left'/'right'
Custom Path.path(d, w, h)any SVG path data

Labels

Simple labels

Add a text label to any node with .label(text, opts?):

builder.node('a').at(100, 100).rect(80, 40).label('Hello');
builder
.node('b')
.at(200, 100)
.circle(30)
.label('World', { fontSize: 14, fill: 'white' });

Multi-line and word-wrap

Use \n for manual line breaks, or maxWidth for automatic word wrapping:

OptionTypeDescription
maxWidthnumberTriggers automatic word wrapping
lineHeightnumberVertical spacing multiplier (default: 1.2)
verticalAlign'top' | 'middle' | 'bottom'Alignment (default: 'middle')
fontSizenumber | stringFont size
fontWeightnumber | stringFont weight
fillstringText color

Rich text labels

Use .richLabel((l) => ...) for mixed-format labels — bold, italic, code, links, superscripts:

Styling

Fill and stroke

builder
.node('a')
.at(100, 100)
.rect(80, 40)
.fill('#2196F3')
.stroke('#1565C0', 2);

Border dash patterns

Presetstroke-dasharray
'solid'none
'dashed'8, 4
'dotted'2, 4
'dash-dot'8, 4, 2, 4

Drop shadows

Add a drop shadow with .shadow() (defaults) or .shadow({ dx, dy, blur, color }):

Sketch / hand-drawn mode

Apply a hand-drawn effect with .sketch() on individual nodes, edges, or globally:

ScopeAPIDescription
Per-node.sketch() / .sketch({ seed })Sketchy rendering for one node
Per-edge.sketch()Sketchy rendering for one edge
Globalviz().sketch()Apply to all nodes and edges

Embedded media

Images

Embed images with .image(href, w, h, opts?). Position options: 'center', 'above', 'below', 'left', 'right'.

Icons and inline SVG

Use .icon(id, opts) with the icon registry, or .svgContent(markup, opts) for inline SVG:

Built-in icons: 'database', 'server', 'cloud', 'person'.

Positioning

Two ways to position nodes:

  • .at(x, y) — absolute coordinates in the viewBox.
  • .cell(col, row) — grid-based positioning (requires .grid() on the builder).

Text badges

Attach 1–2 character indicators (class kind, status icon, etc.) pinned to each corner of a node.

Options

OptionTypeDefaultDescription
positionBadgePosition'top-left'Corner to pin the badge to
fillstringText colour
backgroundstringPill background colour (omit for none)
fontSizenumber10Font size in px

BadgePosition is one of 'top-left' · 'top-right' · 'bottom-left' · 'bottom-right'.

Declarative form

viz().node('n', {
badges: [
{ text: 'C', position: 'top-left', fill: '#fff', background: '#4a9eff' },
{ text: '⚡', position: 'top-right', fill: '#e64553' },
],
});

Compartmented nodes (UML-style sections)

Divide a node into horizontal compartments separated by divider lines — ideal for UML class boxes.

Use .compartment(id, cb?) on a NodeBuilder to define each section. The builder auto-sizes the node height to fit all compartments.

When compartments are present, the main .label() is suppressed — each compartment carries its own label instead.

Declarative compartments

builder.node('cls', {
at: { x: 250, y: 110 },
rect: { w: 200, h: 0, rx: 6 },
fill: '#f5f5f5',
stroke: '#333',
compartments: [
{ id: 'name', label: 'MyClass', height: 36 },
{ id: 'attrs', label: '- field: string' },
{ id: 'methods', label: '+ doWork()' },
],
});

Per-entry interactivity

Use .entry(id, text, opts?) inside a compartment callback to make each line independently interactive — with its own click handler, tooltip, hover highlight, and text styling.

Entries are rendered as individual SVG <text> elements with data-viz-role="compartment-entry" and data-entry attributes. Hovered entries receive the CSS class viz-entry-hover.

Entry padding and custom CSS class

Control vertical spacing around each entry with padding, and attach a custom CSS class with className for targeted styling.

padding accepts a single number (uniform top & bottom) or an object { top?: number; bottom?: number } for asymmetric spacing. The entry height grows to include the padding, and text remains vertically centered within the content area.

The className value is appended to the entry's internal CSS class string, so you can target entries with standard CSS:

.entry-danger text {
fill: #c00;
}

Declarative entries

builder.node('cls', {
at: { x: 250, y: 110 },
rect: { w: 200, h: 0, rx: 6 },
compartments: [
{ id: 'name', label: 'MyClass', height: 36 },
{
id: 'methods',
entries: [
{ id: 'get', text: '+ getUser()' },
{ id: 'set', text: '+ setUser()', style: { fontWeight: 'bold' } },
{
id: 'rm',
text: '- removeUser()',
padding: 4,
className: 'entry-danger',
},
],
},
],
});

Compartment hit-testing

hitTest() returns a compartmentId when the pointer lands inside a specific compartment, so you can build context menus or selection per-section.

When the compartment uses entries, hitTest() also returns an entryId identifying the specific entry that was hit.

Collapsed / compact mode

Use .collapsed() to render only the first compartment (the header) while preserving all compartment data. This is useful for compact UML views where you want to hide attributes and methods until the user expands.

When collapsed:

  • Only the first compartment (header) is rendered; dividers and subsequent compartments are hidden.
  • A small triangle indicator appears in the header, signaling the collapsed state.
  • The node auto-sizes its height to the first compartment only.
  • All compartment data is preserved in the scene — toggling back to expanded restores the full view.
  • The node group receives the CSS class viz-node-collapsed for styling hooks.

Interactive collapse toggle

Use .onClick(handler) on a compartment to wire up collapse/expand behavior. The handler receives a CompartmentClickContext with a toggle() helper:

Click the triangle indicator in the header to toggle between collapsed and expanded states.

When the first compartment has an onClick handler, a clickable collapse indicator appears in the header — even when the node is expanded. Clicking the indicator fires the first compartment's onClick.

The CompartmentClickContext provides:

FieldTypeDescription
nodeIdstringThe id of the node
compartmentIdstringThe id of the compartment
collapsedbooleanCurrent collapsed state (before toggling)
collapseAnchorCollapseAnchorCurrent collapse anchor ('center' when unset)
toggle(opts?: { animate?: number; anchor?: CollapseAnchor }) => voidToggle collapse with optional animation and anchor override

Declarative collapsed

builder.node('cls', {
at: { x: 200, y: 200 },
rect: { w: 160, h: 0, rx: 6 },
collapsed: true,
compartments: [
{
id: 'name',
label: 'ClassName',
height: 36,
onClick: (ctx) => ctx.toggle(),
},
{ id: 'attrs', label: '- field: string' },
{ id: 'methods', label: '+ doWork()' },
],
});

Customising the collapse indicator

The collapse chevron can be styled, hidden, or replaced with custom SVG:

// Hide the indicator entirely
builder
.node('no-chevron')
.at(100, 100)
.rect(160, 0)
.compartment('header', (c) => c.label('Title').onClick((ctx) => ctx.toggle()))
.compartment('body', (c) => c.label('Content'))
.collapseIndicator(false)
.done();

// Custom colour
builder
.node('red-chevron')
.at(300, 100)
.rect(160, 0)
.compartment('header', (c) => c.label('Title').onClick((ctx) => ctx.toggle()))
.compartment('body', (c) => c.label('Content'))
.collapseIndicator({ color: 'crimson' })
.done();

// Custom SVG via render callback
builder
.node('custom')
.at(500, 100)
.rect(160, 0)
.compartment('header', (c) => c.label('Title').onClick((ctx) => ctx.toggle()))
.compartment('body', (c) => c.label('Content'))
.collapseIndicator({
render: (collapsed) =>
`<text font-size="12">${collapsed ? '▶' : '▼'}</text>`,
})
.done();

The same options work declaratively:

builder.node('cls', {
at: { x: 200, y: 200 },
rect: { w: 160, h: 0 },
compartments: [
{ id: 'name', label: 'Title', onClick: (ctx) => ctx.toggle() },
{ id: 'body', label: 'Content' },
],
collapseIndicator: { color: 'blue' },
});

See CollapseIndicatorOptions for all options.

Collapse anchor

Control which edge stays fixed during collapse/expand animation with .collapseAnchor():

  • 'top' — top edge stays fixed; the node shrinks/grows downward.
  • 'center' — the node shrinks/grows symmetrically (default).
  • 'bottom' — bottom edge stays fixed; the node shrinks/grows upward.

Select an anchor, then click the collapse indicator on either node to see how the animation direction changes.

You can also pass the anchor per-toggle via ctx.toggle({ animate: 300, anchor: 'top' }), which overrides the node-level default.

CSS classes

Apply a CSS class with .class('my-class') for theming:

builder.node('a').at(100, 100).rect(80, 40).class('highlight');

Then style via CSS:

.highlight .viz-node-shape {
fill: gold;
stroke: orange;
}

Found a problem? Open an issue on GitHub.