Skip to content

Blocks & Ports ​

A block is any DOM element you already control — the engine only measures it (and its ports), and never touches its content or layout beyond an optional drag transform.

Defining a block ​

BlockDescriptor

id ​

string

Unique block id, referenced by a connection's from.blockId/to.blockId.

el ​

HTMLElement

The block's own element.

ports ​

PortDescriptor[] · default: — (unset)

Explicit ports — see "Defining a port" below. Omitted, connections anchor directly to the block's own border, with side: 'auto'.

draggable ​

boolean · default: follows blocks.draggable

Overrides the instance-level default for this one block.

dragHandle ​

string | HTMLElement · default: — (the whole block starts a drag)

A CSS selector, or an element inside el, that starts the drag instead of the whole block.

dragBounds ​

DragBounds · default: follows blocks.drag.bounds

Overrides the instance-level default for this one block. See "Drag & drop" below for the three accepted shapes.

portSpread ​

boolean | { gap?: number; padding?: number } · default: follows ports.spread

Spreads the connections that share a port side of this block into separate virtual ports. A port's own spread overrides it. See Port Spreading.

highlightable ​

boolean · default: follows interaction.highlight

Turns on or off the highlight state of this block's connections while the pointer is over the block; the local value wins over the global one. See Visual States.

Example — every field at once:

ts
linker.setBlocks([
  {
    id: 'task-1', // unique id — connections reference this via from.blockId/to.blockId
    el: taskCardEl, // the block's own element
    ports: [{ id: 'output', side: 'right', offset: 0.5 }], // optional, see "Defining a port" below — omit entirely to anchor to the block's own border instead
    draggable: true, // this block can be dragged even if blocks.draggable is false
    dragHandle: '.card-header', // only this part of the block starts a drag — omit to let the whole block start one
    dragBounds: 'container', // this block can never be dragged outside the diagram's own container
    portSpread: { gap: 20 }, // connections sharing a side of this block each get their own virtual port, 20px apart
    highlightable: true, // pointing at this block highlights its connections, even if interaction.highlight is false
  },
])

Defining a port ​

PortDescriptor

A block's ports array lets a connection anchor somewhere more specific than the block's own border — a particular row in a list, a specific field in a form.

id ​

string

Port id, referenced by a connection endpoint's portId.

target ​

string | HTMLElement · default: — (the block's own element)

A CSS selector (resolved against the block's el) or a direct element — where the port's connector point actually sits.

side ​

PortSide | FixedSide[] · default: 'auto'

'auto' resolves the exit/entry side from the other endpoint's position on every render, picking whichever side best faces it. A FixedSide[] (a subset of 'top' | 'right' | 'bottom' | 'left') narrows that automatic choice — e.g. ['left', 'right'] to rule out top/bottom exits entirely. A single fixed side pins it outright.

offset ​

number · default: 0.5

Position along the resolved side, 0..1 (0.5 is centered). Ignored when anchorBlockId/anchorEl is set.

anchorBlockId ​

string · default: — (unset)

Renders this port's connector point on another registered block's own border instead of target's — useful when target is a child nested well inside a container (e.g. a row inside a group block) but the connection should visually leave from the container's own edge. The point still tracks target's actual position, projected onto the anchor block's border, so siblings anchored to the same block keep their relative order instead of collapsing to one spot. Side resolution (including 'auto') is based on the anchor block, not target. Ignored when anchorEl is also set.

anchorEl ​

HTMLElement · default: — (unset)

Like anchorBlockId, but anchors directly to a given element instead of a registered block's own el — for anchoring to an element that isn't (or isn't yet) one of the engine's registered blocks. Takes precedence over anchorBlockId when both are set.

spread ​

boolean | { gap?: number; padding?: number } · default: follows the block's portSpread

Spreads the connections that share this port's side. false opts the port out even when its block spreads. See Port Spreading.

Example — every field at once:

ts
{
  id: 'output', // referenced by a connection's from.portId/to.portId
  target: '.output-row', // a CSS selector inside the block — omit to use the block's own element
  side: ['right', 'bottom'], // 'auto' can only ever pick right or bottom here, never top/left
  offset: 0.25, // ignored in this example, since anchorBlockId below overrides where the point actually sits
  anchorBlockId: 'group-1', // the point is drawn on block "group-1"'s own border, while still tracking target's real position
  spread: true, // connections sharing this port's side get their own virtual ports
}

Setting anchorEl: someElement instead of anchorBlockId anchors to that exact element rather than a registered block — and wins if both happen to be set at once.

Drag & drop ​

Any block can be made draggable — set blocks.draggable: true in the configuration for every block, or draggable on a single block (BlockDescriptor.draggable, wins when set). Dragging is pointer-driven, moves the block through the standalone CSS translate property — so it composes with any transform the block already has instead of overwriting it — and every incident connection re-routes in real time as the block moves, no manual re-render call needed.

ts
const linker = createVisualLinker(diagramEl, {
  blocks: {
    draggable: true, // every block can be dragged unless it sets draggable: false itself
    drag: {
      grid: 20, // dragged blocks snap to a shared 20px grid, so independently moved blocks still line up
      bounds: 'container', // no block can be dragged outside the diagram's own container
    },
  },
})

linker.setBlocks([
  {
    id: 'task-1', // this block's id
    el: taskCardEl, // this block's own element
    dragHandle: '.card-header', // only the header starts a drag — the rest of the card is inert
  },
  {
    id: 'task-2', // this block's id
    el: otherCardEl, // this block's own element
    draggable: false, // opts this one block out, even though blocks.draggable above is true
  },
])

dragBounds (for every block via blocks.drag.bounds, or per block via BlockDescriptor.dragBounds) confines a draggable block's position, clamped so the block's own rect never leaves the bounds:

  • 'container' — the engine's own container element, as used above.
  • an HTMLElement — an arbitrary element's box (e.g. a dedicated drop-zone elsewhere in the layout, not necessarily the container itself).
  • a DragBoundsInset object ({ top?, right?, bottom?, left? }, in px, unset edges default to 0) — the container's own box, shrunk by these paddings, e.g. { top: 16, bottom: 16 } to keep dragged blocks clear of a header/footer overlapping the container.

Unset on both levels — unconstrained, the default behavior.

Drag events (block:dragstart/block:drag/block:dragend) are covered on the Engine API page.