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
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 noportIdand 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: thelineslook, dashed, opacity0.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. Returnfalseto forbid, or a string with the reason, which is read out by the live region and arrives inconnection: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 }.viais'drag' | 'click' | 'keyboard';modifiersis{ shift, ctrl, alt, meta }as held at the finish.fromandtoareConnectionEndpoints, in the order of the gesture;tohas noportIdwhen the end is a block.connection:create-cancel—{ from, reason, message? }.reasonis'escape' | 'no-target' | 'invalid' | 'pointer-cancel' | 'api';messageis the string avalidatereturned.
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 atfrom;falsewhen the mode is off or the end is not allowed. It works whatevermodeis.completeConnect(to)—boolean. Finishes attoand emitsconnection:create-requestwithvia: 'click';falsewhen nothing is running ortois 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).
.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. Setannounceto your own texts — strings with{from}and{to}, or functions of{ from, to, message }. prefers-reduced-motionswitches off the animated flow; the temporary line is not animated.
In 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.