VisualLinker Component
Draws connections between elements you place yourself. Put any template into the default slot — blocks can sit at any depth, inside any wrapper components — and mark them with the v-vl-block/v-vl-port directives, plain data-vl-* attributes, or pass them through the blocks prop. The component adds no wrapper per block and never touches your markup or layout: it only measures the marked elements and draws the lines in a layer of its own.
<script setup lang="ts">
import { VisualLinker, vVlBlock } from '@macrulez/visual-linker-vue'
const connections = [{ id: 'a-b', from: { blockId: 'a' }, to: { blockId: 'b' } }]
</script>
<template>
<VisualLinker :connections="connections">
<MyLayout>
<MyCard v-vl-block="'a'" />
<SidePanel>
<div data-vl-block="b">Plain HTML works too</div>
</SidePanel>
</MyLayout>
</VisualLinker>
</template>How to mark blocks and ports, and which <VisualLinker> an element belongs to, is covered on Marking Blocks & Ports.
Props
connections
ConnectionDescriptor[] · required
See Connections. Watched deeply — mutating an existing connection's style in place re-renders too.
blocks
VisualLinkerBlock[] · default: []
Blocks passed explicitly, for elements held as refs in <script> or elements you can't add attributes to. Same shape as core's BlockDescriptor (see Blocks & Ports), with every element-valued field also accepting a Vue template ref or getter:
el— a template ref, a getter, a plain element, or a CSS selector. A selector is resolved inside the component's area, or across the whole document withscope="page". Withoutel, the entry only adds config (ports, drag options) to a block already marked in the template with the sameid.ports— a port'starget/anchorElaccept a template ref as well.draggable,dragHandle,dragBounds— as in core.
For the same id, a field set here wins over the directive's options, which win over data attributes.
config
VisualLinkerConfig · default: {}
The diagram's configuration — see Configuration. It is reactive: replace the object, or change a reactive one, and the diagram redraws with the new values; a theme switch or a toggled lines.jumps needs no remount. Fields left unset fall back to the shared configuration of the app (what the Nuxt module and VisualLinkerPlugin provide), merged field by field with this config winning, and then to the built-in values.
selected
string[] · default: — (unset, the engine owns the selection)
Ids of the selected connections, for v-model:selected — needs config.interaction.selectable. See Selection & Accessibility.
scope
'container' | 'page' · default: 'container'
'container'— blocks live anywhere inside the component's default slot; lines are drawn in a layer inside the component's own box.'page'— blocks may live anywhere in the document; lines are drawn in a viewport-sizedposition: fixedlayer teleported to<body>, so an ancestor'soverflow: hiddenortransformcan't clip or offset it.dragBounds: 'container'then means the viewport.
name
string · default: — (unset)
Lets elements elsewhere claim this instance via data-vl-linker="<name>" or the directives' linker option. Give each instance a name when several page-scoped ones share a page.
zIndex
number | string · default: — (unset)
z-index of the layer the lines (and overlay slots) are drawn in.
Any other attribute (class, style, id, …) lands on the component's root element.
Emits
block-dragstart
Payload: { blockId: string }
block-drag
Payload: { blockId: string; x: number; y: number }
block-dragend
Payload: { blockId: string; x: number; y: number }
block-mouseenter
Payload: { blockId: string }
block-mouseleave
Payload: { blockId: string }
connection-click
Payload: ConnectionDescriptor
connection-mouseenter
Payload: ConnectionDescriptor
connection-mouseleave
Payload: ConnectionDescriptor
connection-selectionchange
Payload: string[] — the ids of the selected connections.
connection-delete-request
Payload: ConnectionDescriptor[] — the connections to delete. Nothing is removed; you decide.
update:selected
Payload: string[] — for v-model:selected.
The first eight mirror the engine's own events (see Engine API) one-to-one, just with kebab-case event names instead of the engine's block:/connection:-prefixed ones.
Slots
default
Scope: Without scope
Your own markup — the blocks and ports marked inside it are picked up at any depth. Elements added, removed, or re-marked later (a v-if, a v-for, third-party markup setting data-vl-* attributes) are picked up automatically.
connection-label
Scope: { connection: ConnectionDescriptor; label?: ConnectionLabel; point: Point; angle?: number; rotation?: number; from: Point; to: Point }
Rendered once per connection, absolutely positioned at the connection's actual midpoint (curve-aware, not just the endpoints' midpoint). For a connection with labels, it is rendered once per label that has no text instead, at that label's point, rotated by rotation — labels with text are drawn by the engine — an HTML overlay inside the drawing layer, since arbitrary Vue content can't render inside an <svg> without a <foreignObject>'s cross-browser quirks. Only built when this slot is actually used.
port
Scope: { blockId: string; portId?: string; point: Point }
Rendered once per resolved port — for custom port content instead of (or, with ports.show, alongside) the built-in dot. Only built when this slot is actually used.
marker
Scope: { connection: ConnectionDescriptor; position: 'start' | 'end'; point: Point; angle: number }
Rendered per connection endpoint that has no marker configured — neither in config.markers nor in the connection's style.markers — and isn't pinned to the edge of a scrolling container (see Scrolling Containers) — a configured marker still wins and renders as a native SVG marker. Only built when this slot is actually used.
Example — custom marker and connection label:
<VisualLinker :connections="connections">
<div v-for="item in items" :key="item.id" v-vl-block="item.id" class="card">{{ item.id }}</div>
<template #connection-label="{ connection }">
<span class="badge">{{ connection.id }}</span>
</template>
<template #marker="{ position }">
<span v-if="position === 'end'" class="dot" />
</template>
</VisualLinker>Page scope
Example — linking two lists rendered by different components:
<template>
<VisualLinker scope="page" name="assign" :connections="connections" :z-index="10" />
<TaskList>
<li v-vl-block="{ id: 't1', linker: 'assign' }">Task 1</li>
</TaskList>
<OwnerList>
<li data-vl-block="ann" data-vl-linker="assign">Ann</li>
</OwnerList>
</template>The <VisualLinker> itself can stay empty here — with scope="page" it doesn't need to wrap the blocks it connects.
Rendered markup
The component renders a single root <div class="vl-container"> around your slot content (with position: relative in container scope). The drawing layer — <div class="vl-layer">, plus vl-layer--page in page scope — only appears client-side after mount, so the server render and the hydration pass stay identical. In container scope the layer is the root's last child, so it paints over the slot content without any z-index juggling.