Blog / Use cases

Sub-flows, groups, containers: why nesting breaks every flow library

August 22, 2026 · use cases · groups & containers

diagrams dashboards react

Look at the top-voted feature requests of any flow library and one theme towers over the rest: users want to put things inside other things. In React Flow's tracker it's "Nested flows", "Nested Nodes" and "absolute positioning in sub flows" — sixty-plus upvotes between them, one still open. This isn't a React Flow problem. It's that nesting looks like a styling feature and is actually a semantics feature, and half-shipping it is worse than not shipping it.

Why it's genuinely hard

The moment a node can contain nodes, every subsystem gets a second opinion:

  • Dragging: does moving the parent move the children? (Yes.) Does dragging a child near the edge leave the parent? (Your call — but it must be a call, not an accident. One of the top-voted react-flow Stack Overflow questions is literally "one node drags other nodes" — accidental containment.)
  • Hit-testing: a click inside a container over empty space — who gets it? The container? The canvas? The deepest nested thing under the pointer?
  • Serialization: if membership isn't part of the document, a saved diagram reloads as loose boxes over a decorative rectangle.
  • Undo: dragging a node from one container into another touches two containers and a membership record. One gesture must be one undo step.
  • Collapse: fold a container and its links must re-anchor to the header — and unfold must restore exactly what was there, even after a save/reload.

Libraries that treat groups as a rendering trick get the first demo right and the next four bullets wrong — which is exactly why the feature requests stay open for years. The correct-but-expensive answer is that containment has to live in the model.

Containment is membership

In Grafloria a group is a model object with members, and everything else follows from that one decision:

import { GroupModel } from '@grafloria/engine';

diagram.addGroup(new GroupModel({ id: 'ingest-stage', position: { x: 0, y: 0 },
  size: { width: 320, height: 180 } }));

await engine.addToGroup('ingest-stage', 'a');   // membership is model state
await engine.collapseGroup('ingest-stage');     // reversible: a snapshot rides ON the group
await engine.expandGroup('ingest-stage');       // …so collapsed diagrams survive save/reload

Members drag with the group. Collapse stores member positions and removed links on the group itself, so a collapsed diagram serializes and expands correctly in a different session. Hit-testing is deepest-wins. And because group operations are commands, the cross-container drag really is one ⌘Z.

The dashboard version, since that's where nesting usually ends up

The most common concrete ask behind "nested flows" is a dashboard section: a box of KPIs that lays out its own children and accepts tiles dragged in. In our dashboard kit that's one field on a widget:

{ id: 'kpis', title: 'KPI section', span: 12, columns: 4,
  widgets: [
    { id: 'k1', kind: 'kpi', span: 1, data: { label: 'Revenue',   value: '$6.8M' } },
    { id: 'k2', kind: 'kpi', span: 1, data: { label: 'Customers', value: '1,284' } },
  ] }

Drag a tile across the boundary and the other grid adopts it live; drag it back out and the board takes it back; either way one undo restores everything, and toJSON() reports the tile under whichever parent it's actually in — derived from live membership, not from the array you authored. Resize a child taller than the section and the section grows a row in its board rather than letting the child overflow.

Try both live: group demos · dashboard containers · the concepts: Groups.