Skip to content

Port Display & Roles ​

Ports are what a line ends in and what the user grabs to start a connection. This page covers how they are drawn, how a source differs from a target, how a line stops at the port, and how to keep buttons inside blocks working.

One port, one circle ​

A port is a single circle. The same circle is the end of a line and the handle the user drags, and its size, outline and colors do not change when a connection appears or disappears. While a gesture is running, only the state-dependent fields from ports.connect.* are applied on top.

If a port is drawn as a circle, the separate handle dot for the same port is hidden, so nothing is drawn twice.

Which ports are drawn ​

ports.show decides:

  • false — no circles.
  • true or 'connected' — a circle on every port that has at least one connection. This is the default.
  • 'all' — a circle on every declared port that is not hidden, connected or not. Use it when every row of a list must show where a line can be attached.

Ports of a scrolled-out row follow clipToScrollParents as usual. ports.fadeEdge (px) makes a port fade out over that distance towards the visible edge of its scroller, so a port does not pop in and out.

Styles by role ​

A port's circle is styled in layers, each one overriding the previous:

  1. ports.* — the group style.
  2. ports.source / ports.target — by role (connect: 'source' | 'target' on the port). Each is a state-aware style, like ports.
  3. ports.connected — ports that have at least one connection.
  4. style on the port descriptor (v-vl-port="{ id, style: { fill } }").
  5. ports.sync: 'marker' — the outline of the circle is taken from the marker of the connection, including its hover state.
  6. ports.connect.<state> — the gesture state: source, candidate, target, invalid.

The circle gets the classes vl-port--source / vl-port--target, vl-port--connected, vl-port--<state> and the port's own className. The role and connected classes are added when connecting is enabled, ports.show is 'all', or one of ports.source, ports.target, ports.connected is set; otherwise the markup is the same as without these features.

Drawing the port yourself ​

  • ports.render: 'svg' — the default: the library draws the circle.
  • ports.render: 'dom' — the library draws no circle and no handle. The port is your own element (v-vl-port target, usually a small <span> in the row). The library starts the gesture from it, sets data-vl-connect-state on it, sets aria-pressed="true" on the source. Needs interaction.connect.enabled. Line ends are still computed from the port's position.
  • #port slot — in the Vue component, rendered for every connectable port, not only the connected ones. Its scope is { blockId, portId, point, role, connected, state, hovered }; role is 'source' | 'target' | 'both', connected is the number of connections, state is 'idle' or a gesture state, hovered says whether the pointer is over the port's block. state and hovered are filled when connecting is enabled or ports.show is 'all'. ports.slotPointerEvents: false makes the slot content transparent to the pointer.

Line stops at the port ​

By default a line goes to the center of the port, under the circle. To stop it at the edge:

  • lines.endGap — a number, or { start, end }, in px: the line ends this far before the port, along the side's normal.
  • ports.clearance — a number in px, or 'auto', which is the circle's radius plus its outline width. It is added to endGap.

The port keeps its own position: only the line is shortened. layout events still report the real port point.

Sides by role ​

ports.sides: { source: 'right', target: 'left' } sets the side by role instead of per port. It applies to ports without their own side, to the end of a connection that fell back to a whole block, and to the preview line of a gesture started from a block.

Ends moved to a fallback block ​

A connection with an end on a fallback block (Fallback Ends) can mark that end:

ts
lines: { proxied: { start: 'circle', end: { shape: 'diamond', size: 8 } } }

proxied.start / proxied.end are marker inputs applied only to the moved end. The block that received the lines gets data-vl-proxy-count="N" with the number of lines moved onto it, so a badge such as "+3" can be drawn in CSS or in the template.

A block can be a connection source or target itself (from: 'block', to: 'block'). Interactive elements inside it must keep working, so a press or a click that starts on an element matching interaction.connect.ignore does not start, finish or cancel a connection. The default selector is:

button, a, input, select, textarea, [contenteditable], [data-vl-ignore]

Set your own selector as a string, or false to disable the check. No stopPropagation is needed in your handlers.

Roles of blocks ​

connect: 'source' | 'target' | 'both' | false on a block is checked when a gesture starts and when it finishes, for from: 'block' and to: 'block' in every mode, including the keyboard and startConnect / completeConnect. A block that sets no role takes it from its ports when every port sets one; otherwise it is 'both'.

Attributes on blocks ​

For styling a whole row without :has(), the library sets attributes on the block element:

  • data-vl-connect-state — source, candidate, target or invalid while a gesture runs (the strongest state of the block or its ports wins).
  • data-vl-hover — pointer while the pointer is over the block, line while the pointer is over a line that ends in it. Set only when interaction.hover, interaction.highlight or connecting is on.
  • data-vl-connecting — on the container while a gesture runs.
  • data-vl-proxy-count — see above.

Attributes are removed when the state ends and when the engine is destroyed.

Line groups and overlapping lines ​

  • connection.kind with lines.kinds: { [kind]: ConnectionStyle } — one style for a group of connections. It sits between the group style and the connection's own style.
  • lines.overlap: 'offset' — a line that coincides with another (same ends and path) is moved aside by 3 px per step (alternating sides, for bezier and straight lines) so both stay visible. 'stack' is the default: lines are drawn on top of each other.
  • markers.<position>.units: 'px' — a marker's size is in pixels and does not depend on the line width. The default 'stroke' scales with the width.

Gesture feedback ​

  • interaction.connect.dimOthers — true (opacity 0.35) or a number: other lines are dimmed while a gesture runs.
  • The engine event connect:candidate — { from, candidate, valid } on every change of the candidate (candidate is null when nothing is under the pointer). connect:start carries via ('drag', 'click', 'keyboard' or 'api').
  • block:mouseenter / block:mouseleave carry via: 'line' when they come from hovering a line that ends in the block. When the pointer itself enters the block, the payload is only { blockId }.
  • In Vue, useVisualLinkerConnect(linker) returns the gesture as refs; see Low-Level Composable.