Flow

A canvas of nodes joined by edges, that you pan, zoom and rearrange.

Dark previewReplay previewOpen in new tab

Stage 1

Done

Draft

1,840 words

Stage 2

Now

Edit

Two rounds

Stage 3

Waiting

Fact check

14 claims

Stage 4

Waiting

Legal read

One quote to clear

Stage 5

Waiting

Schedule

Thursday, 09:00

Stage 6

Waiting

Publish

Site and newsletter

Use it to show how things are connected and let somebody move them about: the stages of a pipeline, an automation, the parts of a system. Nodes are boxes with your own contents, edges join them, and the canvas is panned and zoomed.

It lays nothing out for you. Every node is given a position, so for a graph whose shape comes from data you need to work the positions out first. For steps in a fixed order with nothing to rearrange, a list of steps is the simpler thing.

Installation

Copy the folder into components/ui, where you can edit it. It needs the cn helper, class-variance-authority, and the theme's info, success and warning colours.

text
components/ui/flow/
├── Flow.vue
├── FlowBackground.vue … FlowMiniMap.vue
├── context.ts
├── movable.ts
├── flow-paths.ts
└── index.ts

Usage

vue
<script setup lang="ts">
import {
  Flow,
  FlowBackground,
  FlowControls,
  FlowEdge,
  FlowNode,
  FlowNodeContent,
  FlowNodeDescription,
  FlowNodeTitle,
} from "@/components/ui/flow";
</script>

<template>
  <Flow fit-view-on-mount>
    <FlowBackground />
    <FlowNode id="draft" :position="{ x: 40, y: 20 }">
      <FlowNodeContent>
        <FlowNodeTitle>Draft</FlowNodeTitle>
      </FlowNodeContent>
    </FlowNode>
    <FlowNode id="edit" :position="{ x: 40, y: 150 }">
      <FlowNodeContent>
        <FlowNodeTitle>Edit</FlowNodeTitle>
        <FlowNodeDescription>Two rounds</FlowNodeDescription>
      </FlowNodeContent>
    </FlowNode>
    <FlowEdge from="draft" to="edit" arrow />
    <FlowControls />
  </Flow>
</template>

Composition

<Flow>
  <FlowBackground />
  <FlowNode id="…" :position="…">
    <FlowNodeHeader>
      <FlowNodeDescription />
      <FlowNodeBadge />
    </FlowNodeHeader>
    <FlowNodeContent>
      <FlowNodeTitle />
      <FlowNodeDescription />
    </FlowNodeContent>
    <FlowHandle />
  </FlowNode>
  <FlowGroup id="…" :position="…">
    <FlowGroupItem>
      <FlowNodeContent />
    </FlowGroupItem>
  </FlowGroup>
  <FlowEdge from="…" to="…" />
  <FlowControls />
  <FlowMiniMap />
</Flow>
  • FlowBackground: the pattern behind the graph, dots, lines or crosses. It moves and scales with the graph.
  • FlowNode: a box on the graph. id is what edges call it and position is where it starts. Fill it with the node parts below, or with anything of your own.
  • FlowNodeHeader: a strip across the top of a node, ruled off from the rest, with something at each end: a step number and a FlowNodeBadge.
  • FlowNodeContent: the body of a node, carrying the node's padding. Inside a FlowGroupItem it has none, because the item has its own.
  • FlowNodeTitle: what the node is.
  • FlowNodeDescription: a quieter line about the node. It is smaller inside a FlowGroupItem.
  • FlowNodeBadge: a word in a pill for the node's state, tinted with the theme colour named by color. muted is grey, and the default.
  • FlowHandle: a dot on a node's edge that a connection can be drawn from or dropped on. Give it an id to join an edge to it by name.
  • FlowEdge: a line from one node to another: a curve, a routed line or a straight one, optionally dashed, marching and with an arrowhead.
  • FlowGroup: a dashed box with a label, round things that belong together. It is dragged as one and an edge can be joined to it.
  • FlowGroupItem: one thing inside a group that is not a node of its own: an icon beside a FlowNodeContent. It moves with the group, and edges cannot join it.
  • FlowControls: buttons to zoom, fit everything in view and lock the canvas.
  • FlowMiniMap: the whole graph, small, with the part in view marked on it.

Edges that pick their own side

An edge with no from-side, to-side or handle leaves from whichever face of its node is turned towards the other one. Drag one node past another and the edges turn to follow. Name the sides when the direction should not change.

Dark previewReplay previewOpen in new tab

Service

Healthy

site

Sign-up pages

Service

Healthy

api

Three instances

Service

Healthy

subscribers-db

48,210 rows

Service

Healthy

cache

92% hit rate

Service

Degraded

mailer

Sending issue 112

A mind map

bezier edges from one centre, each in a theme colour of its own. With no sides named, a branch can be dragged anywhere round the centre.

Dark previewReplay previewOpen in new tab

A weekend in Porto

14–16 November

Getting there

Train, 2 h 50 min

Where to stay

Three shortlisted

Food

Market on Saturday

Walks

Coast path, 11 km

Budget

£640 for two

Wiring it up

A FlowHandle is somewhere to start a connection. Drag from one to another node and @connect is told which two; the edges are yours to keep. is-valid-connection refuses the ones you do not want, here a second wire between the same two steps.

Dark previewReplay previewOpen in new tab

Sign-up form

Verify email

Welcome message

Add to CRM

Drag from a dot on one step to another step to join them. 1 joined.

Named ports

FlowHandle takes an id, a face and an offset along it, so one face can hold two. Join an edge to it as "node.handle", and that end stays on the port however the node is dragged. Use ports where the side means something: here accepted leaves from one and rejected from the other.

Dark previewReplay previewOpen in new tab

upload

Checks type and size

Accepted

ok

resize

Rejected

held

review

Four ports

gallery

new · approved on the left

Building a graph

The canvas holds no list of nodes or edges. Each is something you render, so adding one is adding to an array in your own state.

Dark previewReplay previewOpen in new tab

Step 1

Drag it anywhere

1 step, 0 links

Moving nodes from outside

position is where a node starts. Keep the positions in state, update them from @node-drag-end, and pass new ones to move the nodes yourself.

Dark previewReplay previewOpen in new tab

Brief

0, 80

Sketches

220, 80

Review

440, 80

Final art

660, 80

Nodes inside a group

A FlowNode inside a FlowGroup is placed relative to the group and moves with it. Give the group a size, because the nodes do not lay it out. confine stops a node's drag at the group's edge. pinned gives the drag to the group, so the node moves only when the group does.

Dark previewReplay previewOpen in new tab

Hub

Hallway

Living room

Lamp

Drag me

Thermostat

Pinned

Kitchen

Speaker

Drag me

Groups and a minimap

FlowGroup holds ordinary elements and moves as one. The edges are joined to the groups by their id. FlowMiniMap shows where the view is on the whole graph.

Dark previewReplay previewOpen in new tab
Edge

Cache

96 locations

Firewall

Blocking 0.3%

Core

Application

Six instances

Database

One primary, two copies

Storage

Uploads

2.1 TB

Backups

Every six hours

API Reference

Flow

PropTypeDefaultDescription
defaultViewportFlowViewport{ x: 0, y: 0, zoom: 1 }Where the canvas looks to begin with.
minZoomnumber0.3The furthest out it can be zoomed.
maxZoomnumber2.5The furthest in it can be zoomed.
panOnDragbooleantrueWhether dragging the pane pans it.
zoomOnPinchbooleantrueWhether a pinch, or Control with the wheel, zooms it.
fitViewOnMountbooleanfalseFits every node into view once they have been measured.
fitViewPaddingnumber48The room left round the nodes when fitting them, in pixels.
isValidConnection(connection: FlowConnection) => boolean—Decides whether a connection may land. Without it, any two different nodes may be joined.
labelstring"Diagram"What a screen reader calls the canvas.
EventDescription
viewport-change(viewport: FlowViewport). As the canvas is panned or zoomed.
node-drag-end(id: string, position: FlowNodePosition). With a node's new position when a drag or a key press has moved it.
connect(connection: FlowConnection). When a connection drawn from a handle lands on another node.

FlowBackground

PropTypeDefaultDescription
variant"dots" | "lines" | "cross" | "none""dots"Dots, a grid of lines, small crosses, or nothing.
gapnumber24The distance from one mark to the next, in pixels at a zoom of 1.
sizenumber—How big a mark is: a dot's radius, a line's thickness, half a cross's arm.

FlowNode

PropTypeDefaultDescription
idstring—What edges and node-drag-end call this node.
positionFlowNodePosition—Where its top left corner is, on the graph.
draggablebooleantrueWhether it can be dragged and moved with the arrow keys.
confinebooleanfalseInside a FlowGroup, stop a drag at the group's edge instead of letting the node leave it. Does nothing outside a group.
pinnedbooleanfalseInside a FlowGroup, take no drag of its own: a drag that starts on it moves the group, and the node goes with it.
selectedbooleanfalseDraws it as the one that has been picked.
moveStepnumber24How far one arrow key press moves it, in pixels. Shift moves four times as far.
EventDescription
pressWhen it is pressed without being dragged, or with Enter or Space. Enter and Space are left alone without a listener.
deleteWhen Delete or Backspace is pressed on it. The keys are left alone without a listener.

FlowGroup

PropTypeDefaultDescription
idstring—What edges and node-drag-end call this group.
positionFlowNodePosition—Where its top left corner is, on the graph.
draggablebooleantrueWhether it can be dragged and moved with the arrow keys.
labelstring—Written in the corner of the group.
size{ width: number; height: number }—A fixed size for the group. Give one when it holds FlowNodes, which are placed inside it rather than laid out by it, so they do not size it.
moveStepnumber24How far one arrow key press moves it, in pixels.
EventDescription
pressAs on FlowNode.
deleteAs on FlowNode.
SlotDescription
defaultWhat the group holds: FlowGroupItems, FlowNodes, or anything of your own.
labelRicher content for the corner label, in place of the label prop.

FlowNodeBadge

PropTypeDefaultDescription
color"primary" | "info" | "success" | "warning" | "destructive" | "muted""muted"Which of the theme's roles it is tinted with. muted is grey.

FlowHandle

PropTypeDefaultDescription
idstring—A name for this handle, so an edge can be joined to it and not only to the node.
position"top" | "right" | "bottom" | "left""right"Which face of the node it sits on.
offsetnumber0.5How far along that face, from 0 to 1.

FlowEdge

PropTypeDefaultDescription
fromFlowEndpointReference—Where it leaves from: a node's id, "node.handle" for one of its handles, or { node, handle }.
toFlowEndpointReference—Where it arrives.
variant"bezier" | "smoothstep" | "step" | "straight""bezier"A curve, a rounded right-angled route, a square one, or a straight line.
animatedbooleanfalseSets the dashes marching towards the target.
dashedbooleanfalseDraws it dashed.
color"primary" | "info" | "success" | "warning" | "destructive"—One of the theme's roles. Without it the edge is a quiet grey.
widthnumber2How thick the line is, in pixels at a zoom of 1.
arrowbooleanfalseDraws an arrowhead where it arrives.
fromSide"top" | "right" | "bottom" | "left"—The face it leaves from. Worked out from where the two nodes are when left out.
toSide"top" | "right" | "bottom" | "left"—The face it arrives at.
radiusnumber8How round the corners of a smoothstep route are.
gapnumber20How far a routed edge runs straight out of a node before it turns.
curvaturenumber0.25How far a bezier bulges when its target is behind its source.

FlowControls

PropTypeDefaultDescription
zoombooleantrueShows the two zoom buttons.
fitbooleantrueShows the button that fits every node into view.
lockbooleantrueShows the button that locks the canvas against panning, zooming and dragging.
stepnumber1.3How much one press of a zoom button zooms.
position"top-left" | "top-right" | "bottom-left" | "bottom-right""bottom-right"Which corner of the canvas they sit in.

FlowMiniMap

PropTypeDefaultDescription
widthnumber120How wide the map is, in pixels.
heightnumber80How tall the map is, in pixels.
position"top-left" | "top-right" | "bottom-left" | "bottom-right""bottom-left"Which corner of the canvas it sits in.

Every part also takes the attributes of the element it renders, and a class.

Notes

Size

The canvas is 24rem tall and as wide as its container. Set a height with class.

It is drawn as a bordered, rounded box. Inside a frame that already has an edge, such as a card or a panel, give it the frame's height and take its own edge off: class="h-full rounded-none border-0". The examples on this page do that.

fit-view-on-mount fits the graph once. Where the room for the graph changes a great deal, as between a phone and a desktop, lay the nodes out for each and give the canvas a key that changes with the layout, so it is fitted again.

Positions

position is the node's top left corner on the graph, in pixels at a zoom of 1. The canvas holds a node's position after that: a drag moves it, and node-drag-end reports where it stopped. To move a node yourself, keep its position in state and pass the new one.

fit-view-on-mount zooms and pans so every node is in view once they have been measured. It never zooms in past 1.

Edges

from and to are node ids, or { node, handle } to join a handle by its id. The faces an edge uses are worked out from where the two nodes are, and change as one is dragged past the other; from-side and to-side fix them.

variant is bezier, smoothstep, step or straight. animated sets dashes marching towards the target, and stops for anyone who has asked for reduced motion. color names one of the theme's roles; without it an edge is a quiet grey.

Panning and zooming

Drag the empty canvas to pan. Zoom with a pinch, or with Control or Command and the wheel. The plain wheel is left to the page, so a diagram in a long page does not trap its scroll. FlowControls has buttons for the same things, and a lock that stops all of it.

Keyboard

The canvas and every node are Tab stops. On the canvas the arrow keys pan, plus and minus zoom and 0 fits everything. On a node the arrow keys move it by move-step, four times as far with Shift, Enter presses it and Delete calls the delete listener.

A connection can only be drawn with a pointer. Where joining nodes matters to everyone, give them another way to do it, such as a menu on the node.

Inside a node

A node is a card by default; pass class to change it. Buttons, links and fields inside a node work as usual and do not start a drag. Put data-no-drag on anything else that should not.

Groups

A group's children are ordinary elements, laid out however you like inside it. They are not nodes: an edge is joined to the group, not to something in it.

Where the parts go

Flow sorts its slot as it renders: FlowBackground is drawn under the graph, FlowControls and FlowMiniMap over it, and everything else in the layer that pans and zooms. Put those three straight inside Flow (a v-if or v-for on them is fine), not inside a component of your own, or they will pan with the graph.

Public exports

  • Components: Flow, FlowBackground, FlowNode, FlowNodeHeader, FlowNodeContent, FlowNodeTitle, FlowNodeDescription, FlowNodeBadge, FlowHandle, FlowEdge, FlowGroup, FlowGroupItem, FlowControls, FlowMiniMap
  • Types: FlowViewport, FlowNodePosition, FlowConnection, FlowEndpoint, FlowEndpointReference, FlowEdgeVariant, FlowColor, FlowCorner, FlowSide, FlowRect, FlowPoint

Accessibility

Keyboard shortcuts and ARIA behavior.

Shortcut Description
TabMove between the canvas and each node.
← + → + ↑ + ↓On the canvas, pan. On a node, move it by move-step.
Shift + ↑On a node, move it four times as far.
+On the canvas, zoom in.
-On the canvas, zoom out.
0On the canvas, fit everything in view.
EnterOn a node, press it.
DeleteOn a node, call its delete listener.
  • The canvas is an application named by label, described as "Diagram". Nodes and groups are described as "Node" and "Group", and a selected node has aria-selected.
  • Handles, edges, the background and the minimap are hidden from screen readers. The controls are a group of labelled buttons, and the lock button reports aria-pressed.