Viewport

The viewport determines which portion of the diagram is visible. AlpineFlow provides extensive options for controlling panning, zooming, coordinate conversion, and viewport animation.

Pan by dragging the background, scroll to zoom (pinch to zoom on touch), and click Fit View to reset:

<div x-data="flowCanvas({
    nodes: [
        { id: 'a', position: { x: 0, y: 0 }, data: { label: 'Pan & zoom me' } },
        { id: 'b', position: { x: 300, y: 0 }, data: { label: 'Drag to pan' } },
        { id: 'c', position: { x: 150, y: 120 }, data: { label: 'Scroll to zoom' } },
    ],
    edges: [
        { id: 'e1', source: 'a', target: 'b' },
        { id: 'e2', source: 'a', target: 'c' },
        { id: 'e3', source: 'b', target: 'c' },
    ],
    background: 'dots',
    fitViewOnInit: true,
    controls: false,
    minZoom: 0.2,
    maxZoom: 4,
})" class="flow-container" style="height: 250px;"
   x-init="document.getElementById('demo-vp-fit').addEventListener('click', () => fitView({ padding: 0.2, duration: 300 }))">
    <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>

Pan and zoom

Enable or disable panning and zooming with config options:

<div x-data="flowCanvas({
    pannable: true,
    zoomable: true,
    minZoom: 0.1,
    maxZoom: 4,
})">

Mouse button control

Restrict which mouse button triggers panning with panOnDrag:

<div x-data="flowCanvas({ panOnDrag: [0] })">

Pass an array of mouse button indices (0 = left, 1 = middle, 2 = right).

Scroll panning

Use scroll events to pan instead of zoom:

<div x-data="flowCanvas({
    panOnScroll: true,
    panOnScrollDirection: 'both',
    panOnScrollSpeed: 1,
})">

panOnScrollDirection accepts 'both', 'horizontal', or 'vertical'. panOnScrollSpeed is a multiplier for scroll-to-pan speed.

Excluding elements from pan and zoom

Two classes let an element opt out of a canvas gesture, so a drag or wheel over it is handled locally instead of moving the viewport:

  • nopan — a press-drag on the element (or anything inside it) won't pan the canvas. Nodes carry this automatically, which is why dragging a node moves the node rather than the viewport. Add it to a custom on-canvas control — a slider, a draggable widget — that needs its own drag.
  • nowheel — a wheel over the element scrolls its content instead of zooming the canvas. Add it to a scrollable panel or list you place on the canvas.
<!-- wheel here scrolls the list; it does not zoom the canvas -->
<div class="nowheel" style="max-height: 200px; overflow-y: auto"> ... </div>

Neither class blocks the other: nopan gates panning only, nowheel gates wheel zoom only. Rename them with the noPanClassName / noWheelClassName config options, or set noWheelClassName to undefined to disable the wheel opt-out entirely.

Keyboard pan

Hold the Space key to enter grab-cursor pan mode. While held, click and drag to pan the viewport regardless of other interaction settings.

Viewport element

The viewport is the pannable, zoomable layer that contains all nodes. Place it as a direct child of the flowCanvas() scope element using the x-flow-viewport directive:

<div x-data="flowCanvas({ ... })" class="flow-container">
    <div x-flow-viewport>
        <template x-for="node in nodes" :key="node.id">
            <div x-flow-node="node">...</div>
        </template>
    </div>
</div>

The viewport element:

  • Applies the viewport transform (translate + scale) to the element
  • Creates and manages the edge SVG layer as its first child
  • Reactively renders edge SVG elements from the edges array
  • Handles edge visibility (hidden/filtered nodes hide their edges)
  • Removes pre-rendered static edges when reactive edges take over (SSR hydration)
  • Gets a .flow-viewport CSS class automatically

Edges are rendered automatically -- you don't write edge markup. Node templates go inside the viewport via x-for.

fitView

Fit all nodes into the viewport with optional padding and animation:

$flow.fitView({ padding: 0.2, duration: 300 })
  • padding -- Fraction of viewport to leave as margin (default 0.1).
  • duration -- Animation duration in milliseconds. Omit or set to 0 for instant.

On initialization

Automatically fit the view when the canvas first renders:

<div x-data="flowCanvas({ fitViewOnInit: true })">

Via directive

Use the x-flow-action directive on a button:

<button x-flow-action:fit-view>Fit View</button>

Pan around, then click Fit View to animate back:

<div x-data="flowCanvas({
    nodes: [
        { id: 'a', position: { x: 0, y: 0 }, data: { label: 'Node A' } },
        { id: 'b', position: { x: 300, y: 0 }, data: { label: 'Node B' } },
        { id: 'c', position: { x: 150, y: 120 }, data: { label: 'Node C' } },
    ],
    edges: [
        { id: 'e1', source: 'a', target: 'b' },
        { id: 'e2', source: 'a', target: 'c' },
    ],
    background: 'dots',
    fitViewOnInit: true,
    controls: false,
})" class="flow-container" style="height: 250px;"
   x-init="document.getElementById('demo-fitview-btn').addEventListener('click', () => fitView({ padding: 0.2, duration: 300 }))">
    <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>

setViewport / panBy / setCenter

setViewport

Set an exact viewport position and zoom level:

$flow.setViewport({ x: 100, y: 200, zoom: 1.5 }, { duration: 300 })

panBy

Pan the viewport by a relative offset in screen pixels:

$flow.panBy(dx, dy)

setCenter

Center the viewport on specific flow coordinates:

$flow.setCenter(x, y, zoom)
<div x-data="flowCanvas({
    nodes: [
        { id: 'a', position: { x: 0, y: 0 }, data: { label: 'Node A' } },
        { id: 'b', position: { x: 300, y: 0 }, data: { label: 'Node B' } },
        { id: 'c', position: { x: 150, y: 120 }, data: { label: 'Node C' } },
    ],
    edges: [
        { id: 'e1', source: 'a', target: 'b' },
        { id: 'e2', source: 'a', target: 'c' },
    ],
    background: 'dots',
    fitViewOnInit: true,
    controls: false,
})" class="flow-container" style="height: 250px;"
   x-init="
        document.getElementById('demo-vpapi-set').addEventListener('click', () => setViewport({ x: -50, y: -30, zoom: 1.5 }, { duration: 300 }));
        document.getElementById('demo-vpapi-pan').addEventListener('click', () => panBy(-80, -40));
        document.getElementById('demo-vpapi-center').addEventListener('click', () => setCenter(150, 60, 2));
        document.getElementById('demo-vpapi-reset').addEventListener('click', () => fitView({ padding: 0.2, duration: 300 }));
   ">
    <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>

fitBounds

Fit a specific rectangular area into the viewport:

$flow.fitBounds(
    { x: 0, y: 0, width: 500, height: 300 },
    { padding: 0.1, duration: 300 }
)

Useful for focusing on a subset of nodes or a particular region of the diagram.

Click each button to focus on a different region:

<div x-data="flowCanvas({
    nodes: [
        { id: 'a', position: { x: 0, y: 0 }, data: { label: 'Left A' } },
        { id: 'b', position: { x: 0, y: 100 }, data: { label: 'Left B' } },
        { id: 'c', position: { x: 500, y: 0 }, data: { label: 'Right A' } },
        { id: 'd', position: { x: 500, y: 100 }, data: { label: 'Right B' } },
    ],
    edges: [
        { id: 'e1', source: 'a', target: 'b' },
        { id: 'e2', source: 'c', target: 'd' },
    ],
    background: 'dots',
    fitViewOnInit: true,
    controls: false,
})" class="flow-container" style="height: 250px;"
   x-init="
        document.getElementById('demo-bounds-left').addEventListener('click', () => fitBounds({ x: -20, y: -20, width: 200, height: 180 }, { padding: 0.2, duration: 300 }));
        document.getElementById('demo-bounds-right').addEventListener('click', () => fitBounds({ x: 480, y: -20, width: 200, height: 180 }, { padding: 0.2, duration: 300 }));
        document.getElementById('demo-bounds-all').addEventListener('click', () => fitView({ padding: 0.2, duration: 300 }));
   ">
    <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>

Coordinate conversion

Convert between screen (DOM) coordinates and flow (diagram) coordinates:

// Screen position to flow position
const flowPos = $flow.screenToFlowPosition(clientX, clientY)

// Flow position to screen position
const screenPos = $flow.flowToScreenPosition(flowX, flowY)

These are essential when handling native DOM events (e.g., mouse clicks) and mapping them onto the diagram's coordinate space.

Viewport boundaries

Restrict how far the user can pan with translateExtent:

<div x-data="flowCanvas({
    translateExtent: [[-1000, -1000], [2000, 2000]],
})">

The value is [[minX, minY], [maxX, maxY]] in flow coordinates. The viewport will not pan beyond these bounds.

Try panning — the viewport stops at the boundary edges. To also constrain node positions, use nodeExtent:

INTERACTIVE
<div x-data="flowCanvas({
    nodes: [
        { id: 'a', position: { x: 50, y: 30 }, data: { label: 'Pan is limited' } },
        { id: 'b', position: { x: 300, y: 30 }, data: { label: 'Try it' } },
    ],
    edges: [
        { id: 'e1', source: 'a', target: 'b' },
    ],
    translateExtent: [[-100, -100], [600, 200]],
    nodeExtent: [[-100, -100], [600, 200]],
    background: 'dots',
    fitViewOnInit: true,
    controls: 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>

Viewport culling

Viewport culling skips rendering nodes and edges that are outside the visible area, improving performance for large diagrams:

<div x-data="flowCanvas({
    viewportCulling: 'auto',
    cullingAutoThreshold: 150,
    cullingBuffer: 100,
})">
  • viewportCulling -- 'auto' (default), true, or false. With 'auto', culling activates automatically once the node count reaches cullingAutoThreshold. true/false force it on/off regardless of node count.
  • cullingAutoThreshold -- Node count at which viewportCulling: 'auto' turns culling on (default 150).
  • cullingBuffer -- Extra pixels beyond the viewport edge before an element is culled (default 100). A larger buffer prevents pop-in when panning quickly.

Auto-pan

Automatically pan the viewport when the user drags a node or connection near the canvas edge:

<div x-data="flowCanvas({
    autoPanOnNodeDrag: true,
    autoPanOnConnect: true,
    autoPanSpeed: 1,
})">
  • autoPanOnNodeDrag -- Pan while dragging a node near the edge.
  • autoPanOnConnect -- Pan while drawing a connection near the edge.
  • autoPanSpeed -- Speed multiplier for auto-pan velocity.

Events

Listen for viewport changes on the canvas element:

Event Description
viewport-change Fires whenever the viewport position or zoom changes
viewport-move-start Fires when a pan/zoom interaction begins
viewport-move Fires continuously during a pan/zoom interaction
viewport-move-end Fires when a pan/zoom interaction ends
<div
    x-data="flowCanvas()"
    @viewport-change="console.log('viewport:', $event.detail)"
>