Skip to content

Creating Connections ​

The user draws a connection: drags from one port to another, clicks two ports in turn, or does the same with the keyboard. The library does not add anything itself. It reports the request with connection:create-request, and your code decides — adds the connection to its data, asks the server, or refuses. This is the same split as connection:delete-request.

The mode is off by default. With it off there are no handles, no tabindex on ports and no live region, and the output is the same as before.

Turning it on ​

ts
const linker = createVisualLinker(diagramEl, {
  interaction: { connect: { enabled: true } },
})

linker.on('connection:create-request', ({ from, to }) => {
  connections.push({ id: `${from.portId}>${to.portId}`, from, to })
  linker.setConnections(connections)
})

In Vue, listen to @connection-create-request and mark the ports (v-vl-port) — see "In Vue" below.

Options ​

interaction.connect is a ConnectConfig:

  • enabled — boolean · default: false.
  • mode — 'drag' | 'click' | 'both' · default: 'both'. How a connection starts: by pressing on a handle and dragging, by clicking it and then clicking the target, or either.
  • from — 'port' | 'block' | 'both' · default: 'port'. What can start a connection. 'block' allows the block itself (its body), not only a port.
  • to — 'port' | 'block' | 'both' · default: 'port'. What can finish it. With 'block' the end has no portId and the line meets the block border (side: 'auto').
  • handle — 'hover' | 'always' | 'never' · default: 'hover'. The visible dot at a port: while the pointer is over its block, always, or never. The clickable area stays either way.
  • snap — number, px · default: 24. The distance from the pointer within which the line is pulled to the nearest allowed port.
  • allowSelf — boolean · default: false. A connection from a block to itself.
  • allowDuplicates — boolean · default: false. A second connection between the same ordered pair of ends.
  • autoScroll — boolean · default: false. While dragging near the top or bottom edge of a scrolling list, the list scrolls.
  • preview — LineStyle & LineOptions · default: the lines look, dashed, opacity 0.8. The look of the temporary line (curve, color, width, dashed, opacity).
  • announce — { start, candidate, complete, cancel, invalid } · default: English texts. See "Accessibility" below.
  • validate — (from, to) => boolean | string · default: unset. Return false to forbid, or a string with the reason, which is read out by the live region and arrives in connection:create-cancel.message.

validate is a function, so it can be set only in the config of createVisualLinker(), of <VisualLinker> and of useVisualLinker(). The Nuxt module takes JSON only: its options type has no validate, and a function passed there is dropped with a warning.

Which ends can connect ​

A port or block says what it may be with connect:

  • 'source' — a connection can start here.
  • 'target' — a connection can end here.
  • 'both' — either. This is the default while the mode is on.
  • false — not part of the mode at all: no handle, no focus stop.

connect can be set on a block and on a port; the port's value wins over its block's. In markup it is the data-vl-connect attribute (source, target, both, false) and the connect option of v-vl-block / v-vl-port.

The three ways to connect ​

Drag. Press on a port's handle, move — a dashed line follows the pointer — and release over another allowed port. Allowed targets are highlighted; within snap pixels the line jumps to the nearest one. Releasing over an allowed port emits connection:create-request; releasing over empty space, over a forbidden port, pressing Escape or a pointer cancel emits connection:create-cancel. A press that does not move counts as the first click of the two-click way (in mode 'both').

Two clicks. Click a handle, then click an allowed port. A click on empty space or Escape cancels. The temporary line follows the pointer between the clicks.

Keyboard. Ports that can connect become focusable (tabindex="0", role="button", aria-label — Port: block / port, or the port's own ariaLabel; values you already set are kept). Enter or Space on a source starts; Tab, Shift+Tab and the arrow keys move over the allowed targets in document order; Enter or Space finishes; Escape cancels. Focus stays within the allowed ports until then.

A gesture that starts on a port handle is always a connection. On the rest of a block that is draggable, the gesture still drags the block. With from: 'block' a block body starts a connection by drag only when the block is not draggable; a click on the body starts the two-click way in modes 'click' and 'both'.

Events ​

  • connect:start — { from }. A connection was started.
  • connection:create-request — { from, to, via, modifiers }. via is 'drag' | 'click' | 'keyboard'; modifiers is { shift, ctrl, alt, meta } as held at the finish. from and to are ConnectionEndpoints, in the order of the gesture; to has no portId when the end is a block.
  • connection:create-cancel — { from, reason, message? }. reason is 'escape' | 'no-target' | 'invalid' | 'pointer-cancel' | 'api'; message is the string a validate returned.

From code ​

Your own click handlers can drive the same mode, for example when the rows already have handlers of their own:

  • startConnect(from) — boolean. Starts at from; false when the mode is off or the end is not allowed. It works whatever mode is.
  • completeConnect(to) — boolean. Finishes at to and emits connection:create-request with via: 'click'; false when nothing is running or to is not allowed.
  • cancelConnect(reason?) — cancels with 'api'.
  • isConnecting() — { active, from? }.

With the mode off, all of them return false and do nothing.

Appearance and states ​

While a connection is being made, the ports go through four states:

  • source — where it starts from.
  • candidate — an allowed target.
  • target — an allowed target the line has snapped to.
  • invalid — a forbidden port under the pointer.

Each is styled through ports.connect.{source, candidate, target, invalid} (the fields of a port dot: radius, fill, stroke, strokeWidth, opacity). They sit on top of every other state. The colors come from three theme tokens: connectPreview, connectValid and connectInvalid (--vl-connect-preview-color, --vl-connect-valid-color, --vl-connect-invalid-color).

Your own markup can follow along, because the library marks it with attributes and adds no classes:

  • data-vl-connecting="true" on the container while a connection is being made;
  • data-vl-connect-state="source | candidate | target | invalid" on the port element (or the block element, for a block end).
css
.row[data-vl-connect-state='candidate'] {
  background: #e6f6ee;
}
.row[data-vl-connect-state='invalid'] {
  background: #fbe3e3;
}

All attributes are removed when the gesture ends.

Accessibility ​

  • The mode works fully from the keyboard.
  • State is never conveyed by color alone: the attributes above and the live region say the same.
  • A visually hidden aria-live="polite" region inside the layer announces the start, each candidate, the result and a refusal. Set announce to your own texts — strings with {from} and {to}, or functions of { from, to, message }.
  • prefers-reduced-motion switches off the animated flow; the temporary line is not animated.

In Vue ​

vue
<script setup lang="ts">
const config = {
  interaction: {
    connect: {
      enabled: true,
      validate: (from, to) => !taken(to) || 'This field is already mapped',
    },
  },
}
function onCreate({ from, to }) {
  rules.value.push({ id: `${from.portId}>${to.portId}`, from, to })
}
</script>

<template>
  <VisualLinker
    ref="linker"
    :connections="rules"
    :config="config"
    @connection-create-request="onCreate"
    @connection-create-cancel="(e) => log(e.reason)"
  >
    <div v-vl-block="'source'">
      <div v-vl-port="{ id: 'email', side: 'right' }">email</div>
    </div>
    <div v-vl-block="'target'">
      <div v-vl-port="{ id: 'mail', side: 'left', connect: 'target' }">mail</div>
    </div>
  </VisualLinker>
</template>

The component emits connect-start, connection-create-request and connection-create-cancel. Through a template ref it exposes startConnect, completeConnect, cancelConnect, isConnecting, batch and stats; useVisualLinker() returns the same functions.