Handle Positions
The x-flow-handle directive marks an element as a connection handle -- either a source (initiates connections) or a target (receives connections). Handles must be placed inside an element with x-flow-node.
Default positions — target on top, source on bottom:
<div x-data="flowCanvas({
nodes: [
{ id: 'a', position: { x: 0, y: 0 }, data: { label: 'Node A' } },
{ id: 'b', position: { x: 250, y: 0 }, data: { label: 'Node B' } },
],
edges: [
{ id: 'e1', source: 'a', target: 'b' },
],
background: 'dots',
fitViewOnInit: true,
controls: false,
pannable: false,
zoomable: false,
})" class="flow-container" style="height: 220px;">
<div x-flow-viewport>
<template x-for="node in nodes" :key="node.id">
<div x-flow-node="node">
<div x-flow-handle:target></div>
<span x-text="node.data.label"></span>
<div x-flow-handle:source></div>
</div>
</template>
</div>
</div>
Argument
The argument is required and specifies the handle type:
| Argument | Description |
|---|---|
:source |
Initiates connections (cursor changes to crosshair) |
:target |
Receives connections |
<div x-flow-node="node">
<div x-flow-handle:target></div>
<span x-text="node.data.label"></span>
<div x-flow-handle:source></div>
</div>
Position modifiers
Control where the handle is placed on the node border:
| Modifier | Description | Default for |
|---|---|---|
.top |
Top edge | target |
.right |
Right edge | -- |
.bottom |
Bottom edge | source |
.left |
Left edge | -- |
When no position modifier is provided, the default is bottom for source handles and top for target handles.
Left-to-right flow — source on right, target on left:
<div x-data="flowCanvas({
nodes: [
{ id: 'a', position: { x: 0, y: 0 }, data: { label: 'Source' } },
{ id: 'b', position: { x: 300, y: 0 }, data: { label: 'Target' } },
],
edges: [
{ id: 'e1', source: 'a', target: 'b' },
],
background: 'dots',
fitViewOnInit: true,
controls: false,
pannable: false,
zoomable: false,
})" class="flow-container" style="height: 220px;">
<div x-flow-viewport>
<template x-for="node in nodes" :key="node.id">
<div x-flow-node="node">
<div x-flow-handle:target.left></div>
<span x-text="node.data.label"></span>
<div x-flow-handle:source.right></div>
</div>
</template>
</div>
</div>
Corner placement
Position modifiers can be combined for corner placement:
| Compound modifier | Position |
|---|---|
.top.left |
Top-left |
.top.right |
Top-right |
.bottom.left |
Bottom-left |
.bottom.right |
Bottom-right |
<!-- Target at top-left corner with custom ID -->
<div x-flow-handle:target.top.left="'tl'"></div>
<div x-data="flowCanvas({
nodes: [
{ id: 'a', position: { x: 150, y: 0 }, data: { label: 'Corner Handles' } },
],
edges: [],
background: 'dots',
fitViewOnInit: true,
controls: false,
pannable: false,
zoomable: false,
})" class="flow-container" style="height: 220px;">
<div x-flow-viewport>
<template x-for="node in nodes" :key="node.id">
<div x-flow-node="node">
<div x-flow-handle:target.top.left="'tl'"></div>
<div x-flow-handle:target.top.right="'tr'"></div>
<span x-text="node.data.label"></span>
<div x-flow-handle:source.bottom.left="'bl'"></div>
<div x-flow-handle:source.bottom.right="'br'"></div>
</div>
</template>
</div>
</div>
Dynamic position via expression
The expression is optional and can be either a string (handle ID) or an object with id and position properties.
String expression (handle ID)
<div x-flow-handle:source="'output'"></div>
<div x-flow-handle:target="'input'"></div>
When omitted, the handle ID defaults to the argument name ("source" or "target").
Object expression (ID and dynamic position)
<div x-flow-handle:source="{ id: 'out', position: dynamicPos }"></div>
| Property | Type | Description |
|---|---|---|
id |
string |
Handle identifier (falls back to type) |
position |
HandlePosition |
Reactively updates the handle's position |
This is useful when the handle position needs to change at runtime based on component state.
Click the buttons to move the source handle — the edge re-routes automatically:
<div x-data="flowCanvas({
nodes: [
{ id: 'a', position: { x: 0, y: 0 }, data: { label: 'Dynamic' }, sourcePosition: 'bottom' },
{ id: 'b', position: { x: 300, y: 0 }, data: { label: 'Target' }, targetPosition: 'left' },
],
edges: [
{ id: 'e1', source: 'a', target: 'b' },
],
background: 'dots',
fitViewOnInit: true,
controls: false,
pannable: false,
zoomable: false,
})" class="flow-container" style="height: 220px;"
x-init="
document.getElementById('demo-dynpos-top').addEventListener('click', () => { getNode('a').sourcePosition = 'top' });
document.getElementById('demo-dynpos-right').addEventListener('click', () => { getNode('a').sourcePosition = 'right' });
document.getElementById('demo-dynpos-bottom').addEventListener('click', () => { getNode('a').sourcePosition = 'bottom' });
document.getElementById('demo-dynpos-left').addEventListener('click', () => { getNode('a').sourcePosition = 'left' });
">
<div x-flow-viewport>
<template x-for="node in nodes" :key="node.id">
<div x-flow-node="node">
<div x-flow-handle:target></div>
<span x-text="node.data.label"></span>
<div x-flow-handle:source></div>
</div>
</template>
</div>
</div>
Per-node handle positions
Set default handle positions on individual nodes with sourcePosition and targetPosition. Handles without a position modifier inherit the node's setting:
nodes: [
{ id: 'a', position: { x: 0, y: 0 }, data: { label: 'Left to Right' },
sourcePosition: 'right', targetPosition: 'left' },
{ id: 'b', position: { x: 0, y: 150 }, data: { label: 'Top to Bottom' },
sourcePosition: 'bottom', targetPosition: 'top' },
]
This is useful when different nodes in the same flow need different orientations — for example, a horizontal pipeline feeding into a vertical decision tree.
<div x-data="flowCanvas({
nodes: [
{ id: 'a', position: { x: 0, y: 50 }, data: { label: 'L->R Node' }, sourcePosition: 'right', targetPosition: 'left' },
{ id: 'b', position: { x: 250, y: 50 }, data: { label: 'L->R Node' }, sourcePosition: 'right', targetPosition: 'left' },
{ id: 'c', position: { x: 500, y: 0 }, data: { label: 'T->B Node' }, sourcePosition: 'bottom', targetPosition: 'top' },
{ id: 'd', position: { x: 500, y: 130 }, data: { label: 'T->B Node' }, sourcePosition: 'bottom', targetPosition: 'top' },
],
edges: [
{ id: 'e1', source: 'a', target: 'b' },
{ id: 'e2', source: 'b', target: 'c' },
{ id: 'e3', source: 'c', target: 'd' },
],
background: 'dots',
fitViewOnInit: true,
controls: false,
pannable: false,
zoomable: false,
})" class="flow-container" style="height: 250px;">
<div x-flow-viewport>
<template x-for="node in nodes" :key="node.id">
<div x-flow-node="node">
<div x-flow-handle:target></div>
<span x-text="node.data.label"></span>
<div x-flow-handle:source></div>
</div>
</template>
</div>
</div>
Position resolution order
Handle position is resolved in this priority order:
- Compound modifier (e.g.,
.top.left) - Single modifier (e.g.,
.right) - Object expression
positionproperty data-flow-handle-positionHTML attribute- Parent node's
sourcePosition/targetPositionproperty (reactive) - Default:
bottomfor source,topfor target
Multiple handles
A node can have any number of source and target handles. Add multiple x-flow-handle directives and position them independently:
<div x-flow-node="node">
<div x-flow-handle:target.left="'in-1'" style="top: 25%;"></div>
<div x-flow-handle:target.left="'in-2'" style="top: 75%;"></div>
<span x-text="node.data.label"></span>
<div x-flow-handle:source.right="'out-1'" style="top: 25%;"></div>
<div x-flow-handle:source.right="'out-2'" style="top: 75%;"></div>
</div>
When a node has multiple handles of the same type, each handle needs a unique name to distinguish it. Pass a string expression to the directive to assign the name:
<!-- Named handles -->
<div x-flow-handle:source.right="'out-1'"></div>
<div x-flow-handle:source.right="'out-2'"></div>
<!-- Unnamed (defaults to 'source' / 'target') -->
<div x-flow-handle:source></div>
<div x-data="flowCanvas({
nodes: [
{ id: 'a', position: { x: 0, y: 20 }, data: { label: 'Multi Output' } },
{ id: 'b', position: { x: 300, y: 0 }, data: { label: 'Target A' } },
{ id: 'c', position: { x: 300, y: 120 }, data: { label: 'Target B' } },
],
edges: [
{ id: 'e1', source: 'a', sourceHandle: 'out-1', target: 'b', targetHandle: 'target' },
{ id: 'e2', source: 'a', sourceHandle: 'out-2', target: 'c', targetHandle: 'target' },
],
background: 'dots',
fitViewOnInit: true,
controls: false,
pannable: false,
zoomable: false,
})" class="flow-container" style="height: 250px;">
<div x-flow-viewport>
<template x-for="node in nodes" :key="node.id">
<template x-if="node.id === 'a'">
<div x-flow-node="node" style="min-height: 70px; display: flex; align-items: center; justify-content: center;">
<div x-flow-handle:target.left></div>
<span x-text="node.data.label"></span>
<div x-flow-handle:source.right="'out-1'" style="top: 25%;"></div>
<div x-flow-handle:source.right="'out-2'" style="top: 75%;"></div>
</div>
</template>
</template>
<template x-for="node in nodes" :key="'t-' + node.id">
<template x-if="node.id !== 'a'">
<div x-flow-node="node">
<div x-flow-handle:target.left></div>
<span x-text="node.data.label"></span>
<div x-flow-handle:source.right></div>
</div>
</template>
</template>
</div>
</div>
Routing edges to named handles
Reference handle names on edges with sourceHandle and targetHandle to route connections to specific ports:
edges: [
{ id: 'e1', source: 'nodeA', sourceHandle: 'out-1', target: 'nodeB', targetHandle: 'in-1' },
{ id: 'e2', source: 'nodeA', sourceHandle: 'out-2', target: 'nodeC', targetHandle: 'in-1' },
]
When omitted, edges connect to the first handle of the matching type. When specified, the edge path starts or ends at the exact position of the named handle — even if multiple handles share the same side of the node.
This pattern is the foundation for building data-flow editors, pipeline builders, and any diagram where nodes have distinct input/output ports.
Hidden modifier
The .hidden modifier hides the handle visually while keeping it functional:
<div x-flow-handle:source.hidden></div>
CSS classes
| Class | Applied when |
|---|---|
.flow-handle |
Always |
.flow-handle-source |
Handle type is source |
.flow-handle-target |
Handle type is target |
.flow-handle-active |
Handle is being hovered or snapped during drag |
.flow-handle-valid |
Target can accept the pending connection |
.flow-handle-invalid |
Target cannot accept the pending connection |
.flow-handle-limit-reached |
Connection rejected specifically due to limit |
During a connection drag, all target handles in the canvas receive either .flow-handle-valid or .flow-handle-invalid so you can style valid drop targets differently from invalid ones.