Interactions

Pan and zoom

Drag the pane to pan. Scroll or pinch to zoom.

Source code
FlowDiagram({
  data: { nodes, edges },
  viewport: { fitView: true, minZoom: 0.5, maxZoom: 2 },
  interaction: {
    pane: {
      drag: "pan",         // or "select"
      scroll: "zoom",      // or "pan"
      doubleClick: "zoom", // or "none"
      autoPan: { speed: 15 },
    },
  },
});

Touch

On a touch screen, drag with one finger and pinch with two.

Source code
interaction: {
  touch: true,
  pane: { pinch: true },
  contextMenu: { longPress: 500 }, // ms
}

Drag nodes

Drag a node. ⌘-click a second one, then drag both.

Source code
interaction: {
  drag: { enabled: true, threshold: 1 },
},
on: {
  dragEnd: (_event, node, nodes) => save(nodes),
},

// Keep one node inside its parent
{ id: "a", parentId: "lane", extent: "parent", ... }

Select

Shift-drag on the pane to box-select. Press Esc to clear.

Source code
interaction: {
  select: { box: true, mode: "partial" },
},
custom: {
  selectionBox: ({ rect }) => MySelectionBox(rect),
},
on: {
  selectionChange: ({ nodes }) => inspector.show(nodes),
},

Hover neighbours

Hover a node. Its neighbours light up.

Source code
custom: {
  node: ({ node, isHovered, isNeighbour }) =>
    Card(node.data.label, {
      highlight: isHovered || isNeighbour,
    }),
},

Bring to front

Click the card underneath. It rises above the others.

Source code
interaction: {
  select: { elevate: true }, // the default
},

// Pin a layer yourself
{ id: "note", zIndex: 10, ... }

Connect

Drag from a handle onto another node. Drag an edge end to reconnect.

Source code
interaction: {
  connect: {
    mode: "strict",  // source → target only
    radius: 20,
    onClick: true,   // click two handles
    reconnect: true,
    validate: (c) => c.source !== c.target,
  },
},
on: { connect: (c) => console.log(c) },

Drop on the pane

Drag from a handle and release on empty space.

Source code
const flow = new FlowDiagramController();

FlowDiagram({
  data: { nodes, edges }, controller: flow,
  on: {
    connectEnd: ({ state, dropPosition }) => {
      if (state.toNode) return;
      const id = crypto.randomUUID();
      flow.addNodes({ id, position: dropPosition, data: { label: "New" } });
      flow.addEdges({ id: `e-${id}`, source: state.fromNode.id, target: id });
    },
  },
});

Context menu

Right-click a node, an edge or the pane.

Source code
interaction: { contextMenu: { enabled: true, longPress: 500 } },
custom: {
  contextMenu: ({ target, ids, position }, flow) =>
    Menu(position, [
      ["Duplicate", () => flow.duplicate(ids)],
      ["Delete", () => flow.deleteElements(ids)],
    ]),
},

Delete

Select a node and press Backspace. Start refuses to go.

Source code
interaction: {
  keys: { delete: ["Backspace", "Delete"] }, // null turns it off
},
on: {
  // false vetoes; a { nodes, edges } object patches
  beforeDelete: ({ nodes }) =>
    nodes.every((node) => node.id !== "start"),
},

Undo and redo

Move or delete a node, then press ⌘Z. ⇧⌘Z redoes.

Source code
interaction: { history: { enabled: true, limit: 100 } },
on: {
  historyChange: ({ canUndo, canRedo }) => toolbar.update(canUndo, canRedo),
},

// From your own buttons
flow.history.undo();
flow.run(() => flow.setNodes(tidy), { history: "merge" });

Copy, paste, duplicate

Select a node. Press ⌘C then ⌘V, or ⌘D to duplicate.

Source code
interaction: {
  clipboard: { offset: { x: 20, y: 20 }, system: false },
  keys: { duplicate: null }, // turn one shortcut off
},

// The same mutations without the keys
const content = flow.getContent(["a", "b"]);
flow.putContent(content, { at: flow.pointerFlowPosition });

Keyboard

Click a node, then press the arrow keys. Shift moves ten times as far.

Source code
interaction: {
  keys: {
    selectAll: "Meta+a",
    nudge: ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"],
    fitView: "Shift+1",
    zoomIn: ["+", "="],
    zoomOut: "-",
    escape: "Escape",
    duplicate: null, // null disables a binding
  },
},

Groups

Drag the loose step into the stage, then out again.

Source code
const nodes = [
  { id: "stage", type: "group", width: 300, height: 180, position, data: {} },
  { id: "a", parentId: "stage", position: { x: 20, y: 50 }, data },
  { id: "loose", position: { x: 360, y: 60 }, data },
];

interaction: { drag: { dropIntoGroup: true } },
custom: {
  node: (args, ctx) => args.node.type === "group"
    ? Frame({ glow: args.isDropTarget })
    : defaultNode(args, ctx),
},

Layout

Pick an algorithm. Nodes move into place and stay draggable.

Source code
import { layeredLayout } from "@flitterjs/diagram";

FlowDiagram({
  data: { nodes, edges },
  layout: {
    algorithm: layeredLayout, // tree, force, grid, circular
    options: { direction: "LR", rankSpacing: 80 },
    runOn: "mount",           // or "manual" + flow.layout()
  },
});

Read-only

Nodes stay put. Pan, zoom, hover and click still work.

Source code
interaction: "viewer",

// Or keep the editor and lock one thing
can: (gesture, target) =>
  !(gesture === "drag" && target.id === "locked"),

Inline text edit

Double-click a label. Enter commits, Esc cancels.

Source code
interaction: { edit: { text: true } }, // default nodes edit already
custom: {
  node: ({ label }) => Card({ child: label }),
},
on: {
  editEnd: ({ node, committed }) => committed && save(node),
},

Resize

Select the node, then drag a corner or a side.

Source code
interaction: {
  edit: { resize: { minWidth: 80, minHeight: 40 } },
},

// Or per node
{ id: "a", resizable: true, resizeOptions: { keepAspectRatio: true }, ... }

on: {
  resizeEnd: (_event, node, { width, height }) => save(node.id, width, height),
},

Edge waypoints

Click the edge, then drag a dot to bend it. Double-click a bend to remove it.

Source code
interaction: { edit: { waypoints: true } },
edges: [
  { id: "e", source: "a", target: "b", waypoints: [{ x: 200, y: 40 }] },
],
custom: {
  edgeHandle: ({ kind, isActive }) =>
    Dot({ size: kind === "midpoint" ? 6 : 8, filled: isActive }),
},
on: { reshapeEnd: ({ edge }) => save(edge) },

Snap and guides

Drag a node level with another. It snaps and a guide appears.

Source code
interaction: {
  drag: {
    snap: { grid: [20, 20], guides: true, radius: 5 },
  },
},
custom: {
  guides: ({ lines }) => GuideLines(lines, { color: "#e11d48" }),
},

Overlap policy

Drop a node onto another. It is pushed aside when you let go.

Source code
interaction: {
  drag: { overlap: "push" }, // or "reject", default "allow"
},
custom: {
  node: ({ node, isOverlapping }) =>
    Card(node.data.label, { warn: isOverlapping }),
},

// After a paste or a layout
flow.resolveOverlaps();

Collapse and expand

Click the chevron on Lead. The team hides and a count appears.

Source code
{ id: "lead", collapsible: true, position, data }

custom: {
  node: ({ node, isCollapsed, hiddenChildCount }, flow) =>
    Card(node.data.label, {
      badge: isCollapsed ? `+${hiddenChildCount}` : null,
      chevron: Chevron({
        open: !isCollapsed,
        onClick: () => flow.toggleCollapsed(node.id),
      }),
    }),
},
// The default context menu adds Collapse / Expand too

Insert on edge

Hover the edge and click +. A step lands in between.

Source code
edges: [{ id: "e", source: "a", target: "b", label: "+" }],
custom: {
  // The label slot sits on the edge and keeps its hover
  edgeLabel: ({ edge, isHovered, x, y }, flow) =>
    isHovered
      ? PlusButton(() =>
          flow.insertNodeOnEdge(edge.id, {
            id: crypto.randomUUID(),
            position: { x: x - 75, y: y - 18 },
            data: { label: "New step" },
          }))
      : SizedBox.shrink(),
},

Delete and reconnect

Click Remove review. Draft and Publish stay connected.

Source code
GestureDetector({
  onClick: () =>
    flow.deleteElements(["review"], { reconnect: true }),
  child: Button("Remove review"),
})

Align and distribute

Shift-drag around the nodes, then click Align left or Distribute.

Source code
const ids = flow.getSelectedNodes().map((node) => node.id);

flow.alignNodes(ids, "left"); // centerX, right, top, centerY, bottom
flow.distributeNodes(ids, "y"); // or "x"